├── LICENSE.md ├── README.md ├── dm_property_list.h ├── editor.c ├── examples ├── arrow.png ├── cloud_1.dps ├── cloud_1.png ├── directional_rotation_test_1.dps ├── fire.png ├── fire_1.dps ├── multitexture_test.dps ├── multitexture_test_00.png ├── portal_01.png ├── portal_1.dps ├── rect_test.dps ├── ring_1.dps ├── smokey_donut.dps ├── snow.png ├── snowing.dps ├── summoning_animation_00.png ├── summoning_animation_01.png ├── summoning_animation_02.png ├── summoning_animation_03.png ├── summoning_animation_04.png └── summoning_animation_1.dps ├── global.h ├── gui.c ├── particles.h └── screenshot └── screenshot000.png /LICENSE.md: -------------------------------------------------------------------------------- 1 | zlib License 2 | 3 | Copyright (C) 2021 Vlad Adrian (@Demizdor - https://github.com/Demizdor). 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 2. Altered source versions must be plainly marked as such, and must not be 18 | misrepresented as being the original software. 19 | 3. This notice may not be removed or altered from any source distribution. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Particle Editor 5 | 6 | A experimental particle editor + header only particle library (particles.h) in early stages of development (ALPHA version). 7 | Made with [raylib](https://github.com/raysan5/raylib) and [raygui](https://github.com/raysan5/raygui) 8 | 9 | ## Build 10 | 11 | On Linux assumming raylib is compiled as a *.so and headers are in `/usr/local/lib/` just run 12 | `gcc editor.c gui.c -o Editor -I/usr/local/include/ -lraylib -lm -std=c99 -O3` 13 | 14 | On Windows/Mac have no idea, sorry! 15 | *...use a build system you say ...what is that?!* 16 | 17 | ## Features / Usage 18 | - drop a `.dps` file (like the ones in examples) to load a particle system in the editor 19 | - drop a texture when a emitter is active to set/change its texture 20 | - drop a texture when no emitter is active (use the X button) to set/change a placeholder 21 | - CTR+C/CTR+V when mouse is not inside the UI window to copy/paste emitters 22 | - CTR+C/CTR+V when mouse is ove a color widget to copy/paste colors 23 | 24 | 25 | # Bugs 26 | ...over 9000 (well not quite but might be a few) 27 | -------------------------------------------------------------------------------- /dm_property_list.h: -------------------------------------------------------------------------------- 1 | /******************************************************************************************* 2 | * 3 | * PropertyListControl v1.0.1 - A custom control that displays a set of properties as a list 4 | * 5 | * UPDATES: last updated - 23 feb 2021 (v1.0.2) 6 | * v1.0.2 - Added a bunch of things (THIS VERSION IS INCOMPLETE - DO NOT UPDATE) 7 | * v1.0.1 - Made it work with latest raygui version 8 | * - Added `GuiDMSaveProperties()` for saving properties to a text file 9 | * - A `GuiDMLoadProperties()` is planed but not implemented yet 10 | * - Added a section property that can work as a way to group multiple properties 11 | * - Fixed issue with section not having the correct height 12 | * v1.0.0 - Initial release 13 | * 14 | * 15 | * MODULE USAGE: 16 | * #define GUI_PROPERTY_LIST_IMPLEMENTATION 17 | * #include "dm_property_list.h" 18 | * 19 | * INIT: GuiDMProperty props[] = { // initialize a set of properties first 20 | PCOLOR(), 21 | PINT(), 22 | PFLOAT(), 23 | ... 24 | }; 25 | * DRAW: GuiDMPropertyList(bounds, props, sizeof(props)/sizeof(props[0]), ...); 26 | * 27 | * 28 | * NOTE: This module also contains 2 extra controls used internally by the property list 29 | * - GuiDMValueBox() - a value box that supports displaying float values 30 | * - GuiDMSpinner() - a `better` GuiSpinner() 31 | * 32 | * LICENSE: zlib/libpng 33 | * 34 | * Copyright (c) 2020 Vlad Adrian (@Demizdor - https://github.com/Demizdor). 35 | * 36 | **********************************************************************************************/ 37 | 38 | #include 39 | 40 | // WARNING: raygui implementation is expected to be defined before including this header 41 | #undef RAYGUI_IMPLEMENTATION 42 | #include "raygui.h" 43 | 44 | 45 | #ifndef GUI_PROPERTY_LIST_H 46 | #define GUI_PROPERTY_LIST_H 47 | 48 | #ifdef __cplusplus 49 | extern "C" { // Prevents name mangling of functions 50 | #endif 51 | 52 | //---------------------------------------------------------------------------------- 53 | // Defines and Macros 54 | //---------------------------------------------------------------------------------- 55 | 56 | // A bunch of usefull macros for modifying the flags of each property 57 | 58 | // Set flag `F` of property `P`. `P` must be a pointer! 59 | #define PROP_SET_FLAG(P, F) ((P)->flags |= (F)) 60 | // Clear flag `F` of property `P`. `P` must be a pointer! 61 | #define PROP_CLEAR_FLAG(P, F) ((P)->flags &= ~(F)) 62 | // Toggles flag `F` of property `P`. `P` must be a pointer! 63 | #define PROP_TOGGLE_FLAG(P, F) ((P)->flags ^= (F)) 64 | // Checks if flag `F` of property `P` is set . `P` must be a pointer! 65 | #define PROP_CHECK_FLAG(P, F) ((P)->flags & (F)) 66 | 67 | // Some usefull macros for creating properties 68 | 69 | // Create a bool property with name `N`, flags `F` and value `V` 70 | #define PBOOL(N, F, V) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_BOOL, F, .value.vbool = V} 71 | // Create a bool property with name `N`, flags `F` and value `V` 72 | #define PBOOLPTR(N, F, V) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_PBOOL, F, .value.vpbool = V} 73 | // Create a int property with name `N`, flags `F` and value `V` 74 | #define PINT(N, F, V) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_INT, F, .value.vint = {V,0,0,1}} 75 | // Create a ranged int property within `MIN` and `MAX` with name `N`, flags `F` value `V`. 76 | // Pressing the spinner buttons will increase/decrease the value by `S`. 77 | #define PINT_RANGE(N, F, V, S, MIN, MAX) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_INT, F, .value.vint = {V,MIN,MAX,S}} 78 | // Create a int property with name `N`, flags `F` and value `V` 79 | #define PINTPTR(N, F, V) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_PINT, F, .value.vpint = {V,0,0,1}} 80 | // Create a ranged int property within `MIN` and `MAX` with name `N`, flags `F` value `V`. 81 | // Pressing the spinner buttons will increase/decrease the value by `S`. 82 | #define PINTPTR_RANGE(N, F, V, S, MIN, MAX) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_PINT, F, .value.vpint = {V,MIN,MAX,S}} 83 | // Create a float property with name `N`, flags `F` and value `V` 84 | #define PFLOAT(N, F, V) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_FLOAT, F, .value.vfloat = {V,0.f,0.f,1.0f,3}} 85 | // Create a ranged float property within `MIN` and `MAX` with name `N`, flags `F` value `V` with `P` decimal digits to show. 86 | // Pressing the spinner buttons will increase/decrease the value by `S`. 87 | #define PFLOAT_RANGE(N, F, V, S, P, MIN, MAX) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_FLOAT, F, .value.vfloat = {V,MIN,MAX,S,P}} 88 | // Create a float property with name `N`, flags `F` and value `V` 89 | #define PFLOATPTR(N, F, V) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_PFLOAT, F, .value.vpfloat = {V,0.f,0.f,1.0f,3}} 90 | // Create a ranged float property within `MIN` and `MAX` with name `N`, flags `F` value `V` with `P` decimal digits to show. 91 | // Pressing the spinner buttons will increase/decrease the value by `S`. 92 | #define PFLOATPTR_RANGE(N, F, V, S, P, MIN, MAX) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_PFLOAT, F, .value.vpfloat = {V,MIN,MAX,S,P}} 93 | // Create a text property with name `N`, flags `F` value `V` and max text size `S` 94 | #define PTEXT(N, F, V, S) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_TEXT, F, .value.vtext = {V, S} } 95 | // Create a text property with name `N`, flags `F` value `V` and max text size `S` 96 | #define PSELECT(N, F, V, A) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_SELECT, F, .value.vselect = {V, A} } 97 | // Create a 2D vector property with name `N`, flags `F` and the `X`, `Y` coordinates 98 | #define PVEC2(N, F, X, Y) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_VECTOR2, F, .value.v2 = {X, Y} } 99 | // Create a 3D vector property with name `N`, flags `F` and the `X`, `Y`, `Z` coordinates 100 | #define PVEC3(N, F, X, Y, Z) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_VECTOR3, F, .value.v3 = {X, Y, Z} } 101 | // Create a 4D vector property with name `N`, flags `F` and the `X`, `Y`, `Z`, `W` coordinates 102 | #define PVEC4(N, F, X, Y, Z, W) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_VECTOR4, F, .value.v4 = {X, Y, Z, W} } 103 | // Create a 2D vector property with name `N`, flags `F` and the `X`, `Y` coordinates 104 | #define PVEC2PTR(N, F, V) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_PVECTOR2, F, .value.vp2 = V } 105 | // Create a 3D vector property with name `N`, flags `F` and the `X`, `Y`, `Z` coordinates 106 | #define PVEC3PTR(N, F, V) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_PVECTOR3, F, .value.vp3 = V } 107 | // Create a 4D vector property with name `N`, flags `F` and the `X`, `Y`, `Z`, `W` coordinates 108 | #define PVEC4PTR(N, F, V) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_PVECTOR4, F, .value.vp4 = V } 109 | // Create a rectangle property with name `N`, flags `F`, `X`, `Y` coordinates and `W` and `H` size 110 | #define PRECT(N, F, X, Y, W, H) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_RECT, F, .value.vrect = {X, Y, W, H} } 111 | // Create a 3D vector property with name `N`, flags `F` and the `R`, `G`, `B`, `A` channel values 112 | #define PCOLOR(N, F, R, G, B, A) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_COLOR, F, .value.vcolor = {R, G, B, A} } 113 | // Create a collapsable section named `N` with `F` flags and the next `C` properties as children. 114 | // !! A section cannot hold another section as a child !! 115 | #define PSECTION(N, F, C) RAYGUI_CLITERAL(GuiDMProperty){N, GUI_PROP_SECTION, F, .value.vsection = (C)} 116 | 117 | // Get flags of the property at `IDX` 118 | #define PGET_FLAGS(P, IDX) (P[IDX].flags) 119 | // Get name of the property at `IDX` 120 | #define PGET_NAME(P, IDX) (P[IDX].name) 121 | // Get type of the property at `IDX` 122 | #define PGET_TYPE(P, IDX) (P[IDX].type) 123 | // Get value of the property at `IDX` 124 | #define PGET_BOOL(P, IDX) (P[IDX].value.vbool) 125 | #define PGET_BOOLPTR(P, IDX) (P[IDX].value.vpbool) 126 | #define PGET_INT(P, IDX) (P[IDX].value.vint.val) 127 | #define PGET_INTPTR(P, IDX) (P[IDX].value.vpint.val) 128 | #define PGET_FLOAT(P, IDX) (P[IDX].value.vfloat.val) 129 | #define PGET_FLOATPTR(P, IDX) (P[IDX].value.vpfloat.val) 130 | #define PGET_V2(P, IDX) (P[IDX].value.v2) 131 | #define PGET_V2PTR(P, IDX) (P[IDX].value.vp2) 132 | #define PGET_V3(P, IDX) (P[IDX].value.v3) 133 | #define PGET_V3PTR(P, IDX) (P[IDX].value.vp3) 134 | #define PGET_V4(P, IDX) (P[IDX].value.v4) 135 | #define PGET_V4PTR(P, IDX) (P[IDX].value.vp4) 136 | #define PGET_RECT(P, IDX) (P[IDX].value.vrect) 137 | #define PGET_RECTPTR(P, IDX) (P[IDX].value.vprect) 138 | #define PGET_COLOR(P, IDX) (P[IDX].value.vcolor) 139 | #define PGET_COLORPTR(P, IDX) (P[IDX].value.vpcolor) 140 | #define PGET_SELECT_ACTIVE(P, IDX) (P[IDX].value.vselect.active) 141 | // Set value of the property at `IDX` 142 | #define PSET_BOOL(P, IDX, V) (P[IDX].value.vbool=V) 143 | #define PSET_BOOLPTR(P, IDX, V) (P[IDX].value.vpbool=V) 144 | #define PSET_INT(P, IDX, V) (P[IDX].value.vint.val=V) 145 | #define PSET_INTPTR(P, IDX, V) (P[IDX].value.vpint.val=V) 146 | #define PSET_FLOAT(P, IDX, V) (P[IDX].value.vfloat.val=V) 147 | #define PSET_FLOATPTR(P, IDX, V) (P[IDX].value.vpfloat.val=V) 148 | #define PSET_V2(P, IDX, V) (P[IDX].value.v2=V) 149 | #define PSET_V2PTR(P, IDX, V) (P[IDX].value.vp2=V) 150 | #define PSET_V3(P, IDX, V) (P[IDX].value.v3=V) 151 | #define PSET_V3PTR(P, IDX, V) (P[IDX].value.vp3=V) 152 | #define PSET_V4(P, IDX, V) (P[IDX].value.v4=V) 153 | #define PSET_V4PTR(P, IDX, V) (P[IDX].value.vp4=V) 154 | #define PSET_RECT(P, IDX, V) (P[IDX].value.vrect=V) 155 | #define PSET_RECTPTR(P, IDX, V) (P[IDX].value.vprect=V) 156 | #define PSET_COLOR(P, IDX, V) (P[IDX].value.vcolor=V) 157 | #define PSET_COLORPTR(P, IDX, V) (P[IDX].value.vpcolor=V) 158 | #define PSET_SELECT_ACTIVE(P, IDX, V) (P[IDX].value.vselect.active=V) 159 | 160 | //---------------------------------------------------------------------------------- 161 | // Types and Structures Definition 162 | //---------------------------------------------------------------------------------- 163 | enum GuiDMPropertyTypes { 164 | GUI_PROP_BOOL = 0, 165 | GUI_PROP_INT, 166 | GUI_PROP_FLOAT, 167 | GUI_PROP_TEXT, 168 | GUI_PROP_SELECT, 169 | GUI_PROP_VECTOR2, 170 | GUI_PROP_VECTOR3, 171 | GUI_PROP_VECTOR4, 172 | GUI_PROP_RECT, 173 | GUI_PROP_COLOR, 174 | GUI_PROP_SECTION, 175 | 176 | GUI_PROP_PBOOL, 177 | GUI_PROP_PINT, 178 | GUI_PROP_PFLOAT, 179 | GUI_PROP_PVECTOR2, 180 | GUI_PROP_PVECTOR3, 181 | GUI_PROP_PVECTOR4, 182 | GUI_PROP_PRECT, 183 | GUI_PROP_PCOLOR, 184 | }; 185 | 186 | enum GuiDMPropertyFlags { 187 | GUI_PFLAG_COLLAPSED = 1 << 0, // is the property expanded or collapsed? 188 | GUI_PFLAG_DISABLED = 1 << 1, // is this property disabled or enabled? 189 | }; 190 | 191 | // Data structure for each property 192 | typedef struct { 193 | char* name; 194 | short type; 195 | short flags; 196 | union { 197 | bool vbool; 198 | struct { int val; int min; int max; int step; } vint; 199 | struct { float val; float min; float max; float step; int precision; } vfloat; 200 | bool* vpbool; 201 | struct { int* val; int min; int max; int step; } vpint; 202 | struct { float* val; float min; float max; float step; int precision; } vpfloat; 203 | struct { char* val; int size; } vtext; 204 | struct { char* val; int active; } vselect; 205 | int vsection; 206 | Vector2 v2; 207 | Vector3 v3; 208 | Vector4 v4; 209 | Rectangle vrect; 210 | Color vcolor; 211 | Vector2* vp2; 212 | Vector3* vp3; 213 | Vector4* vp4; 214 | Rectangle* vprect; 215 | Color* vpcolor; 216 | } value; 217 | } GuiDMProperty; 218 | 219 | //---------------------------------------------------------------------------------- 220 | // Global Variables Definition 221 | //---------------------------------------------------------------------------------- 222 | //... 223 | 224 | //---------------------------------------------------------------------------------- 225 | // Module Functions Declaration 226 | //---------------------------------------------------------------------------------- 227 | 228 | // A more advanced `GuiValueBox()` supporting float/int values with specified `precision`, cursor movements, cut/copy/paste and 229 | // other keybord shortcuts. Needed by `GuiDMSpinner()` !! 230 | // `precision` should be between 1-7 for float values and 0 for int values (maybe 15 for doubles but that was not tested) 231 | // WARNING: The bounds should be set big enough else the text will overflow and things will break 232 | // WARNING: Sometimes the last decimal value could differ, this is probably due to rounding 233 | double GuiDMValueBox(Rectangle bounds, double value, double minValue, double maxValue, int precision, bool editMode); 234 | 235 | // A more advanced `GuiSpinner()` using `GuiDMValueBox()` for displaying the values. 236 | // This was needed because `GuiSpinner()` can't display float values and editing values is somewhat hard. 237 | // This is by no means perfect but should be more user friendly than the default control provided by raygui. 238 | double GuiDMSpinner(Rectangle bounds, double value, double minValue, double maxValue, double step, int precision, bool editMode); 239 | 240 | // Works just like `GuiListViewEx()` but with an array of properties instead of text. 241 | void GuiDMPropertyList(Rectangle bounds, GuiDMProperty* props, int count, int* focus, int* scrollIndex); 242 | 243 | // Handy function to save properties to a file. Returns false on failure or true otherwise. 244 | bool GuiDMSaveProperties(const char* file, GuiDMProperty* props, int count); 245 | 246 | #ifdef __cplusplus 247 | } 248 | #endif 249 | 250 | #endif // GUI_PROPERTY_LIST_H 251 | 252 | 253 | 254 | /*********************************************************************************** 255 | * 256 | * GUI_PROPERTY_LIST_IMPLEMENTATION 257 | * 258 | ************************************************************************************/ 259 | #if defined(GUI_PROPERTY_LIST_IMPLEMENTATION) 260 | 261 | #include "raygui.h" 262 | 263 | #include // for calloc() 264 | #include // for memmove(), strlen() 265 | #include // for sscanf(), snprintf() 266 | 267 | #ifndef __cplusplus 268 | #if __STDC_VERSION__ >= 199901L 269 | #include // for bool if >= C99 270 | #endif 271 | #endif 272 | 273 | double GuiDMValueBox(Rectangle bounds, double value, double minValue, double maxValue, int precision, bool editMode) { 274 | // FIXME: Hope all those `memmove()` functions are correctly used so we won't leak memory or overflow the buffer !!! 275 | static int framesCounter = 0; // Required for blinking cursor 276 | static int cursor = 0; // Required for tracking the cursor position (only for a single active valuebox) 277 | 278 | enum {cursorTimer = 6, maxChars = 31, textPadding = 2}; 279 | 280 | GuiControlState state = GuiGetState(); 281 | 282 | // Make sure value is in range 283 | if(maxValue != minValue){ 284 | if(value < minValue) value = minValue; 285 | if(value > maxValue) value = maxValue; 286 | } 287 | 288 | char textValue[maxChars + 1] = "\0"; 289 | snprintf(textValue, maxChars, "%.*f", precision, value); // NOTE: use `snprintf` here so we don't overflow the buffer 290 | int len = strlen(textValue); 291 | 292 | bool valueHasChanged = false; 293 | 294 | // Update control 295 | //-------------------------------------------------------------------- 296 | if ((state != GUI_STATE_DISABLED) && !guiLocked) 297 | { 298 | if (editMode) 299 | { 300 | // Make sure cursor position is correct 301 | if(cursor > len) cursor = len; 302 | if(cursor < 0) cursor = 0; 303 | 304 | state = GUI_STATE_PRESSED; 305 | framesCounter++; 306 | 307 | if(IsKeyPressed(KEY_RIGHT) || (IsKeyDown(KEY_RIGHT) && (framesCounter%cursorTimer == 0))) { 308 | // MOVE CURSOR TO RIGHT 309 | ++cursor; 310 | framesCounter = 0; 311 | } else if(IsKeyPressed(KEY_LEFT) || (IsKeyDown(KEY_LEFT) && (framesCounter%cursorTimer == 0))) { 312 | // MOVE CURSOR TO LEFT 313 | --cursor; 314 | framesCounter = 0; 315 | } else if (IsKeyPressed(KEY_BACKSPACE) || (IsKeyDown(KEY_BACKSPACE) && (framesCounter%cursorTimer) == 0)) { 316 | // HANDLE BACKSPACE 317 | if(cursor > 0) { 318 | if(textValue[cursor-1] != '.') { 319 | if(cursor < len ) 320 | memmove(&textValue[cursor-1], &textValue[cursor], len-cursor); 321 | textValue[len - 1] = '\0'; 322 | valueHasChanged = true; 323 | } 324 | --cursor; 325 | } 326 | framesCounter = 0; 327 | } else if (IsKeyPressed(KEY_DELETE) || (IsKeyDown(KEY_DELETE) && (framesCounter%cursorTimer) == 0)) { 328 | // HANDLE DEL 329 | if(len > 0 && cursor < len && textValue[cursor] != '.') { 330 | memmove(&textValue[cursor], &textValue[cursor+1], len-cursor); 331 | textValue[len] = '\0'; 332 | len -= 1; 333 | valueHasChanged = true; 334 | } 335 | } else if (IsKeyPressed(KEY_HOME)) { 336 | // MOVE CURSOR TO START 337 | cursor = 0; 338 | } else if (IsKeyPressed(KEY_END)) { 339 | // MOVE CURSOR TO END 340 | cursor = len; 341 | } else if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_C)) { 342 | // COPY 343 | SetClipboardText(textValue); 344 | } else if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_X)) { 345 | // CUT 346 | SetClipboardText(textValue); 347 | textValue[0] = '\0'; 348 | cursor = len = 0; 349 | value = 0.0; // set it to 0 and pretend the value didn't change 350 | } else if (IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_V)) { 351 | // PASTE 352 | const char* clip = GetClipboardText(); 353 | int clipLen = strlen(clip); 354 | clipLen = clipLen > maxChars ? maxChars : clipLen; 355 | memcpy(textValue, clip, clipLen); 356 | len = clipLen; 357 | textValue[len] = '\0'; 358 | valueHasChanged = true; 359 | } 360 | else { 361 | // HANDLE KEY PRESS 362 | int key = GetKeyPressed(); 363 | if( ((len < maxChars) && (key >= 48) && (key <= 57)) || (key == 46) || (key == 45) ) // only allow 0..9, minus(-) and dot(.) 364 | { 365 | if(precision != 0 && cursor < len) { // when we have decimals we can't insert at the end 366 | memmove(&textValue[cursor], &textValue[cursor-1], len+1-cursor); 367 | textValue[len+1] = '\0'; 368 | textValue[cursor] = (char)key; 369 | cursor++; 370 | valueHasChanged = true; 371 | } 372 | else if(precision == 0) { 373 | if(cursor < len) memmove(&textValue[cursor], &textValue[cursor-1], len+1-cursor); 374 | len += 1; 375 | textValue[len+1] = '\0'; 376 | textValue[cursor] = (char)key; 377 | cursor++; 378 | valueHasChanged = true; 379 | } 380 | } 381 | } 382 | 383 | // Make sure cursor position is correct 384 | if(cursor > len) cursor = len; 385 | if(cursor < 0) cursor = 0; 386 | } 387 | else 388 | { 389 | if (CheckCollisionPointRec(GetMousePosition(), bounds)) 390 | { 391 | state = GUI_STATE_FOCUSED; 392 | if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) framesCounter = 0; 393 | } 394 | } 395 | } 396 | //-------------------------------------------------------------------- 397 | 398 | // Draw control 399 | //-------------------------------------------------------------------- 400 | DrawRectangleLinesEx(bounds, GuiGetStyle(VALUEBOX, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(VALUEBOX, BORDER + (state*3))), guiAlpha)); 401 | 402 | Rectangle textBounds = {bounds.x + GuiGetStyle(VALUEBOX, BORDER_WIDTH) + textPadding, bounds.y + GuiGetStyle(VALUEBOX, BORDER_WIDTH), 403 | bounds.width - 2*(GuiGetStyle(VALUEBOX, BORDER_WIDTH) + textPadding), bounds.height - 2*GuiGetStyle(VALUEBOX, BORDER_WIDTH)}; 404 | 405 | int textWidth = GetTextWidth(textValue); 406 | if(textWidth > textBounds.width) textBounds.width = textWidth; 407 | 408 | if (state == GUI_STATE_PRESSED) 409 | { 410 | DrawRectangle(bounds.x + GuiGetStyle(VALUEBOX, BORDER_WIDTH), bounds.y + GuiGetStyle(VALUEBOX, BORDER_WIDTH), bounds.width - 2*GuiGetStyle(VALUEBOX, BORDER_WIDTH), bounds.height - 2*GuiGetStyle(VALUEBOX, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(VALUEBOX, BASE_COLOR_PRESSED)), guiAlpha)); 411 | 412 | // Draw blinking cursor 413 | // NOTE: ValueBox internal text is always centered 414 | if (editMode && ((framesCounter/20)%2 == 0)) { 415 | // Measure text until the cursor 416 | int textWidthCursor = -2; 417 | if(cursor > 0) { 418 | char c = textValue[cursor]; 419 | textValue[cursor] = '\0'; 420 | textWidthCursor = GetTextWidth(textValue); 421 | textValue[cursor] = c; 422 | } 423 | //DrawRectangle(bounds.x + textWidthCursor + textPadding + 2, bounds.y + 2*GuiGetStyle(VALUEBOX, BORDER_WIDTH), 1, bounds.height - 4*GuiGetStyle(VALUEBOX, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(VALUEBOX, BORDER_COLOR_PRESSED)), guiAlpha)); 424 | DrawRectangle(bounds.x + textWidthCursor + (int)((bounds.width - textWidth - textPadding)/2.0f) + 2, bounds.y + 2*GuiGetStyle(VALUEBOX, BORDER_WIDTH), 1, bounds.height - 4*GuiGetStyle(VALUEBOX, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(VALUEBOX, BORDER_COLOR_PRESSED)), guiAlpha)); 425 | } 426 | } 427 | else if (state == GUI_STATE_DISABLED) 428 | { 429 | DrawRectangle(bounds.x + GuiGetStyle(VALUEBOX, BORDER_WIDTH), bounds.y + GuiGetStyle(VALUEBOX, BORDER_WIDTH), bounds.width - 2*GuiGetStyle(VALUEBOX, BORDER_WIDTH), bounds.height - 2*GuiGetStyle(VALUEBOX, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(VALUEBOX, BASE_COLOR_DISABLED)), guiAlpha)); 430 | } 431 | 432 | GuiDrawText(textValue, textBounds, GUI_TEXT_ALIGN_CENTER, Fade(GetColor(GuiGetStyle(VALUEBOX, TEXT + (state*3))), guiAlpha)); 433 | 434 | value = valueHasChanged ? strtod(textValue, NULL) : value; 435 | 436 | // Make sure value is in range 437 | if(maxValue != minValue){ 438 | if(value < minValue) value = minValue; 439 | if(value > maxValue) value = maxValue; 440 | } 441 | 442 | return value; 443 | } 444 | 445 | 446 | 447 | double GuiDMSpinner(Rectangle bounds, double value, double minValue, double maxValue, double step, int precision, bool editMode) { 448 | GuiControlState state = GuiGetState(); 449 | 450 | Rectangle spinner = { bounds.x + GuiGetStyle(SPINNER, SPIN_BUTTON_WIDTH) + GuiGetStyle(SPINNER, SPIN_BUTTON_PADDING), bounds.y, 451 | bounds.width - 2*(GuiGetStyle(SPINNER, SPIN_BUTTON_WIDTH) + GuiGetStyle(SPINNER, SPIN_BUTTON_PADDING)), bounds.height }; 452 | Rectangle leftButtonBound = { (float)bounds.x, (float)bounds.y, (float)GuiGetStyle(SPINNER, SPIN_BUTTON_WIDTH), (float)bounds.height }; 453 | Rectangle rightButtonBound = { (float)bounds.x + bounds.width - GuiGetStyle(SPINNER, SPIN_BUTTON_WIDTH), (float)bounds.y, (float)GuiGetStyle(SPINNER, SPIN_BUTTON_WIDTH), (float)bounds.height }; 454 | 455 | // Update control 456 | //-------------------------------------------------------------------- 457 | if ((state != GUI_STATE_DISABLED) && !guiLocked) 458 | { 459 | Vector2 mousePoint = GetMousePosition(); 460 | 461 | // Check spinner state 462 | if (CheckCollisionPointRec(mousePoint, bounds)) 463 | { 464 | if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) state = GUI_STATE_PRESSED; 465 | else state = GUI_STATE_FOCUSED; 466 | } 467 | } 468 | //-------------------------------------------------------------------- 469 | 470 | 471 | // Draw control 472 | //-------------------------------------------------------------------- 473 | // Draw value selector custom buttons 474 | // NOTE: BORDER_WIDTH and TEXT_ALIGNMENT forced values 475 | int tempBorderWidth = GuiGetStyle(BUTTON, BORDER_WIDTH); 476 | int tempTextAlign = GuiGetStyle(BUTTON, TEXT_ALIGNMENT); 477 | GuiSetStyle(BUTTON, BORDER_WIDTH, GuiGetStyle(SPINNER, BORDER_WIDTH)); 478 | GuiSetStyle(BUTTON, TEXT_ALIGNMENT, GUI_TEXT_ALIGN_CENTER); 479 | 480 | #if defined(RAYGUI_SUPPORT_ICONS) 481 | if (GuiButton(leftButtonBound, GuiIconText(RICON_ARROW_LEFT_FILL, NULL))) value -= step; 482 | if (GuiButton(rightButtonBound, GuiIconText(RICON_ARROW_RIGHT_FILL, NULL))) value += step; 483 | #else 484 | if (GuiButton(leftButtonBound, "<")) value -= step; 485 | if (GuiButton(rightButtonBound, ">")) value += step; 486 | #endif 487 | 488 | GuiSetStyle(BUTTON, TEXT_ALIGNMENT, tempTextAlign); 489 | GuiSetStyle(BUTTON, BORDER_WIDTH, tempBorderWidth); 490 | 491 | value = GuiDMValueBox(spinner, value, minValue, maxValue, precision, editMode); 492 | 493 | return value; 494 | } 495 | 496 | 497 | 498 | void GuiDMPropertyList(Rectangle bounds, GuiDMProperty* props, int count, int* focus, int* scrollIndex) { 499 | #ifdef RAYGUI_SUPPORT_ICONS 500 | #define PROPERTY_COLLAPSED_ICON "#120#" 501 | #define PROPERTY_EXPANDED_ICON "#121#" 502 | #else 503 | #define PROPERTY_COLLAPSED_ICON "+" 504 | #define PROPERTY_EXPANDED_ICON "-" 505 | #endif 506 | 507 | #define PROPERTY_PADDING 6 508 | #define PROPERTY_ICON_SIZE 16 509 | #define PROPERTY_DECIMAL_DIGITS 3 //how many digits to show (used only for the vector properties) 510 | 511 | // NOTE: Using ListView style for everything !! 512 | GuiControlState state = GuiGetState(); 513 | int propFocused = (focus == NULL)? -1 : *focus; 514 | int scroll = *scrollIndex > 0 ? 0 : *scrollIndex; // NOTE: scroll should always be negative or 0 515 | 516 | // Each property occupies a certain number of slots, highly synchronized with the properties enum (GUI_PROP_BOOL ... GUI_PROP_SECTION) 517 | // NOTE: If you add a custom property type make sure to add the number of slots it occupies here !! 518 | const int propSlots[] = {1,1,1,2,1,3,4,5,5,5,1, 1,1,1,3,4,5,5,5}; 519 | 520 | Rectangle absoluteBounds = {0}; // total bounds for all of the properties (unclipped) 521 | // We need to loop over all the properties to get total height so we can see if we need a scrollbar or not 522 | for(int p=0; p bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH) ? true : false; 534 | if(!useScrollBar && scroll != 0) scroll = 0; // make sure scroll is 0 when there's no scrollbar 535 | 536 | Rectangle scrollBarBounds = {bounds.x + GuiGetStyle(LISTVIEW, BORDER_WIDTH), bounds.y + GuiGetStyle(LISTVIEW, BORDER_WIDTH), 537 | GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH), bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH)}; 538 | 539 | absoluteBounds.x = bounds.x + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING) + GuiGetStyle(DEFAULT, BORDER_WIDTH); 540 | absoluteBounds.y = bounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING) + GuiGetStyle(DEFAULT, BORDER_WIDTH) + scroll; 541 | absoluteBounds.width = bounds.width - 2*(GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING) + GuiGetStyle(DEFAULT, BORDER_WIDTH)); 542 | 543 | if(useScrollBar) { 544 | if(GuiGetStyle(LISTVIEW, SCROLLBAR_SIDE) == SCROLLBAR_LEFT_SIDE) 545 | absoluteBounds.x += GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH); // scrollbar is on the LEFT, adjust bounds 546 | else 547 | scrollBarBounds.x = bounds.x + bounds.width - GuiGetStyle(LISTVIEW, BORDER_WIDTH) - GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH); // scrollbar is on the RIGHT 548 | absoluteBounds.width -= GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH); // adjust width to fit the scrollbar 549 | } 550 | 551 | int maxScroll = absoluteBounds.height + 2*(GuiGetStyle(LISTVIEW, LIST_ITEMS_PADDING) + GuiGetStyle(DEFAULT, BORDER_WIDTH))-bounds.height; 552 | 553 | // Update control 554 | //-------------------------------------------------------------------- 555 | Vector2 mousePos = GetMousePosition(); 556 | // NOTE: most of the update code is actually done in the draw control section 557 | if ((state != GUI_STATE_DISABLED) && !guiLocked) { 558 | if(!CheckCollisionPointRec(mousePos, bounds)) { 559 | propFocused = -1; 560 | } 561 | 562 | if (useScrollBar) 563 | { 564 | int wheelMove = GetMouseWheelMove(); 565 | scroll += wheelMove*count; 566 | if(-scroll > maxScroll) scroll = -maxScroll; 567 | } 568 | } 569 | //-------------------------------------------------------------------- 570 | 571 | 572 | // Draw control 573 | //-------------------------------------------------------------------- 574 | DrawRectangleRec(bounds, Fade(GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)), guiAlpha) ); // Draw background 575 | DrawRectangleLinesEx(bounds, GuiGetStyle(DEFAULT, BORDER_WIDTH), Fade(GetColor(GuiGetStyle(LISTVIEW, BORDER + state*3)), guiAlpha)); // Draw border 576 | 577 | BeginScissorMode(absoluteBounds.x, bounds.y + GuiGetStyle(DEFAULT, BORDER_WIDTH), absoluteBounds.width, bounds.height - 2*GuiGetStyle(DEFAULT, BORDER_WIDTH)); 578 | int currentHeight = 0; 579 | for(int p=0; p= bounds.y && absoluteBounds.y + currentHeight <= bounds.y + bounds.height) 587 | { 588 | Rectangle propBounds = {absoluteBounds.x, absoluteBounds.y + currentHeight, absoluteBounds.width, height}; 589 | Color textColor = Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_NORMAL)), guiAlpha); 590 | int propState = GUI_STATE_NORMAL; 591 | 592 | // Get the state of this property and do some initial drawing 593 | if(PROP_CHECK_FLAG(&props[p], GUI_PFLAG_DISABLED)) { 594 | propState = GUI_STATE_DISABLED; 595 | propBounds.height += 1; 596 | DrawRectangleRec(propBounds, Fade(GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_DISABLED)), guiAlpha)); 597 | propBounds.height -= 1; 598 | textColor = Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_DISABLED)), guiAlpha); 599 | } else { 600 | if(CheckCollisionPointRec(mousePos, propBounds) && !guiLocked) { 601 | if(IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { 602 | propState = GUI_STATE_PRESSED; 603 | //DrawRectangleRec(propRect, Fade(GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_PRESSED)), guiAlpha)); 604 | textColor = Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_PRESSED)), guiAlpha); 605 | } else { 606 | propState = GUI_STATE_FOCUSED; 607 | propFocused = p; 608 | //DrawRectangleRec(propRect, Fade(GetColor(GuiGetStyle(LISTVIEW, BASE_COLOR_FOCUSED)), guiAlpha)); 609 | textColor = Fade(GetColor(GuiGetStyle(LISTVIEW, TEXT_COLOR_FOCUSED)), guiAlpha); 610 | } 611 | } else propState = GUI_STATE_NORMAL; 612 | } 613 | 614 | if(propState == GUI_STATE_DISABLED) GuiSetState(propState); 615 | switch(props[p].type) 616 | { 617 | case GUI_PROP_BOOL: 618 | case GUI_PROP_PBOOL: { 619 | // draw property name 620 | GuiDrawText(props[p].name, (Rectangle){propBounds.x + PROPERTY_PADDING, propBounds.y, propBounds.width/2-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 621 | 622 | // draw property value 623 | const bool locked = guiLocked; 624 | GuiLock(); // lock the checkbox since we changed the value manually 625 | if(props[p].type == GUI_PROP_BOOL) { 626 | if(propState == GUI_STATE_PRESSED) props[p].value.vbool = !props[p].value.vbool; // toggle the property value when clicked 627 | GuiCheckBox((Rectangle){propBounds.x+propBounds.width/2, propBounds.y + height/4, height/2, height/2}, props[p].value.vbool ? "Yes" : "No", props[p].value.vbool); 628 | } 629 | else 630 | { 631 | bool val = false; 632 | if(props[p].value.vpbool != NULL) val = *props[p].value.vpbool; 633 | if(propState == GUI_STATE_PRESSED) val = !val; // toggle the property value when clicked 634 | GuiCheckBox((Rectangle){propBounds.x+propBounds.width/2, propBounds.y + height/4, height/2, height/2}, val ? "Yes" : "No", val); 635 | if(props[p].value.vpbool != NULL) *props[p].value.vpbool = val; 636 | } 637 | if(!locked) GuiUnlock(); // only unlock when needed 638 | } break; 639 | 640 | case GUI_PROP_INT: 641 | // draw property name 642 | GuiDrawText(props[p].name, (Rectangle){propBounds.x + PROPERTY_PADDING, propBounds.y, propBounds.width/2-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 643 | // draw property value 644 | props[p].value.vint.val = GuiDMSpinner((Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 2}, 645 | props[p].value.vint.val, props[p].value.vint.min, props[p].value.vint.max, props[p].value.vint.step, 0, (propState == GUI_STATE_FOCUSED) ); 646 | break; 647 | 648 | case GUI_PROP_PINT: { 649 | // draw property name 650 | GuiDrawText(props[p].name, (Rectangle){propBounds.x + PROPERTY_PADDING, propBounds.y, propBounds.width/2-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 651 | 652 | int val = 0; 653 | if(props[p].value.vpint.val != NULL) val = *props[p].value.vpint.val; 654 | // draw property value 655 | val = GuiDMSpinner((Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 2}, 656 | val, props[p].value.vpint.min, props[p].value.vpint.max, props[p].value.vpint.step, 0, (propState == GUI_STATE_FOCUSED) ); 657 | if(props[p].value.vpint.val != NULL) *props[p].value.vpint.val = val; 658 | } 659 | break; 660 | 661 | case GUI_PROP_FLOAT: 662 | // draw property name 663 | GuiDrawText(props[p].name, (Rectangle){propBounds.x + PROPERTY_PADDING, propBounds.y, propBounds.width/2-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 664 | // draw property value 665 | props[p].value.vfloat.val = GuiDMSpinner((Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 2}, 666 | props[p].value.vfloat.val, props[p].value.vfloat.min, props[p].value.vfloat.max, props[p].value.vfloat.step, props[p].value.vfloat.precision, (propState == GUI_STATE_FOCUSED) ); 667 | break; 668 | 669 | case GUI_PROP_PFLOAT: 670 | // draw property name 671 | GuiDrawText(props[p].name, (Rectangle){propBounds.x + PROPERTY_PADDING, propBounds.y, propBounds.width/2-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 672 | 673 | float val = 0.0f; 674 | if(props[p].value.vpfloat.val != NULL) val = *props[p].value.vpfloat.val; 675 | // draw property value 676 | val = GuiDMSpinner((Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 14}, 677 | val, props[p].value.vpfloat.min, props[p].value.vpfloat.max, props[p].value.vpfloat.step, props[p].value.vpfloat.precision, (propState == GUI_STATE_FOCUSED) ); 678 | val = GuiSlider((Rectangle){propBounds.x + PROPERTY_PADDING, propBounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 11, propBounds.width-PROPERTY_PADDING, 10}, NULL, NULL, val, props[p].value.vpfloat.min, props[p].value.vpfloat.max); 679 | if(props[p].value.vpfloat.val != NULL) *props[p].value.vpfloat.val = val; 680 | break; 681 | 682 | case GUI_PROP_TEXT: { 683 | Rectangle titleBounds = { propBounds.x, propBounds.y, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) }; 684 | // Collapse/Expand property on click 685 | if((propState == GUI_STATE_PRESSED) && CheckCollisionPointRec(mousePos, titleBounds)) 686 | PROP_TOGGLE_FLAG(&props[p], GUI_PFLAG_COLLAPSED); 687 | 688 | // draw property name 689 | GuiDrawText(PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED) ? PROPERTY_COLLAPSED_ICON : PROPERTY_EXPANDED_ICON, titleBounds, GUI_TEXT_ALIGN_LEFT, textColor); 690 | GuiDrawText(props[p].name, (Rectangle){propBounds.x+PROPERTY_ICON_SIZE+PROPERTY_PADDING, propBounds.y, propBounds.width-PROPERTY_ICON_SIZE-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 691 | GuiDrawText(TextFormat("%i/%i", strlen(props[p].value.vtext.val), props[p].value.vtext.size), (Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 2}, GUI_TEXT_ALIGN_LEFT, textColor); 692 | 693 | // draw property value 694 | if(!PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED)) 695 | GuiTextBox((Rectangle){propBounds.x, propBounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)+1, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2}, props[p].value.vtext.val, props[p].value.vtext.size, (propState == GUI_STATE_FOCUSED)); 696 | } break; 697 | 698 | case GUI_PROP_SELECT: { 699 | // TODO: Create a custom dropdownbox control instead of using the raygui combobox 700 | // draw property name 701 | GuiDrawText(props[p].name, (Rectangle){propBounds.x + PROPERTY_PADDING, propBounds.y, propBounds.width/2-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 702 | // draw property value 703 | if(CheckCollisionPointRec(GetMousePosition(), propBounds) && IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { 704 | if(props[p].value.vselect.active > 0) props[p].value.vselect.active -= 1; 705 | } 706 | props[p].value.vselect.active = GuiComboBox((Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 2}, 707 | props[p].value.vselect.val, props[p].value.vselect.active); 708 | } break; 709 | 710 | case GUI_PROP_VECTOR2: case GUI_PROP_VECTOR3: case GUI_PROP_VECTOR4: { 711 | Rectangle titleBounds = { propBounds.x, propBounds.y, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) }; 712 | // Collapse/Expand property on click 713 | if((propState == GUI_STATE_PRESSED) && CheckCollisionPointRec(mousePos, titleBounds)) 714 | PROP_TOGGLE_FLAG(&props[p], GUI_PFLAG_COLLAPSED); 715 | 716 | const char* fmt = ""; 717 | if(props[p].type == GUI_PROP_VECTOR2) fmt = TextFormat("[%.0f, %.0f]", props[p].value.v2.x, props[p].value.v2.y); 718 | else if(props[p].type == GUI_PROP_VECTOR3) fmt = TextFormat("[%.0f, %.0f, %.0f]", props[p].value.v3.x, props[p].value.v3.y, props[p].value.v3.z); 719 | else if(props[p].type == GUI_PROP_VECTOR4) fmt = TextFormat("[%.0f, %.0f, %.0f, %.0f]", props[p].value.v4.x, props[p].value.v4.y, props[p].value.v4.z, props[p].value.v4.w); 720 | 721 | // draw property name 722 | GuiDrawText(PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED) ? PROPERTY_COLLAPSED_ICON : PROPERTY_EXPANDED_ICON, titleBounds, GUI_TEXT_ALIGN_LEFT, textColor); 723 | GuiDrawText(props[p].name, (Rectangle){propBounds.x+PROPERTY_ICON_SIZE+PROPERTY_PADDING, propBounds.y, propBounds.width-PROPERTY_ICON_SIZE-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 724 | GuiDrawText(fmt, (Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 2}, GUI_TEXT_ALIGN_LEFT, textColor); 725 | 726 | // draw X, Y, Z, W values (only when expanded) 727 | if(!PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED)) { 728 | Rectangle slotBounds = { propBounds.x, propBounds.y+GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)+1, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2}; 729 | Rectangle lblBounds = { propBounds.x+PROPERTY_PADDING, slotBounds.y, GetTextWidth("A"), slotBounds.height}; 730 | Rectangle valBounds = { lblBounds.x+lblBounds.width+PROPERTY_PADDING, slotBounds.y, propBounds.width-lblBounds.width-2*PROPERTY_PADDING, slotBounds.height}; 731 | GuiDrawText("X", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 732 | props[p].value.v2.x = GuiDMSpinner(valBounds, props[p].value.v2.x, 0.0, 0.0, 1.0, PROPERTY_DECIMAL_DIGITS, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 733 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 734 | lblBounds.y = valBounds.y = slotBounds.y; 735 | GuiDrawText("Y", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 736 | props[p].value.v2.y = GuiDMSpinner(valBounds, props[p].value.v2.y, 0.0, 0.0, 1.0, PROPERTY_DECIMAL_DIGITS, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 737 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 738 | lblBounds.y = valBounds.y = slotBounds.y; 739 | if(props[p].type >= GUI_PROP_VECTOR3) { 740 | GuiDrawText("Z", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 741 | props[p].value.v3.z = GuiDMSpinner(valBounds, props[p].value.v3.z, 0.0, 0.0, 1.0, PROPERTY_DECIMAL_DIGITS, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 742 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 743 | lblBounds.y = valBounds.y = slotBounds.y; 744 | } 745 | 746 | if(props[p].type >= GUI_PROP_VECTOR4) { 747 | GuiDrawText("W", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 748 | props[p].value.v4.w = GuiDMSpinner(valBounds, props[p].value.v4.w, 0.0, 0.0, 1.0, PROPERTY_DECIMAL_DIGITS, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 749 | } 750 | } 751 | } break; 752 | 753 | case GUI_PROP_PVECTOR2: case GUI_PROP_PVECTOR3: case GUI_PROP_PVECTOR4: { 754 | Rectangle titleBounds = { propBounds.x, propBounds.y, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) }; 755 | // Collapse/Expand property on click 756 | if((propState == GUI_STATE_PRESSED) && CheckCollisionPointRec(mousePos, titleBounds)) 757 | PROP_TOGGLE_FLAG(&props[p], GUI_PFLAG_COLLAPSED); 758 | 759 | const char* fmt = ""; 760 | if(props[p].type == GUI_PROP_PVECTOR2) { 761 | if(props[p].value.vp2 != NULL) 762 | fmt = TextFormat("[%.0f, %.0f]", (*props[p].value.vp2).x, (*props[p].value.vp2).y); 763 | } 764 | else if(props[p].type == GUI_PROP_PVECTOR3) { 765 | if(props[p].value.vp3 != NULL) 766 | fmt = TextFormat("[%.0f, %.0f, %.0f]", (*props[p].value.vp3).x, (*props[p].value.vp3).y, (*props[p].value.vp3).z); 767 | } 768 | else if(props[p].type == GUI_PROP_PVECTOR4) { 769 | if(props[p].value.vp4 != NULL) 770 | fmt = TextFormat("[%.0f, %.0f, %.0f, %.0f]", (*props[p].value.vp4).x, (*props[p].value.vp4).y, (*props[p].value.vp4).z, (*props[p].value.vp4).w); 771 | } 772 | 773 | // draw property name 774 | GuiDrawText(PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED) ? PROPERTY_COLLAPSED_ICON : PROPERTY_EXPANDED_ICON, titleBounds, GUI_TEXT_ALIGN_LEFT, textColor); 775 | GuiDrawText(props[p].name, (Rectangle){propBounds.x+PROPERTY_ICON_SIZE+PROPERTY_PADDING, propBounds.y, propBounds.width-PROPERTY_ICON_SIZE-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 776 | GuiDrawText(fmt, (Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 2}, GUI_TEXT_ALIGN_LEFT, textColor); 777 | 778 | // draw X, Y, Z, W values (only when expanded) 779 | if(!PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED)) { 780 | union { 781 | Vector2 v2; 782 | Vector3 v3; 783 | Vector4 v4; 784 | } val = {0}; 785 | 786 | if(props[p].type == GUI_PROP_PVECTOR2 && props[p].value.vp2 != NULL) val.v2 = *props[p].value.vp2; 787 | else if(props[p].type == GUI_PROP_PVECTOR3 && props[p].value.vp3 != NULL) val.v3 = *props[p].value.vp3; 788 | else if(props[p].type == GUI_PROP_PVECTOR4 && props[p].value.vp4 != NULL) val.v4 = *props[p].value.vp4; 789 | 790 | Rectangle slotBounds = { propBounds.x, propBounds.y+GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)+1, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2}; 791 | Rectangle lblBounds = { propBounds.x+PROPERTY_PADDING, slotBounds.y, GetTextWidth("A"), slotBounds.height}; 792 | Rectangle valBounds = { lblBounds.x+lblBounds.width+PROPERTY_PADDING, slotBounds.y, propBounds.width-lblBounds.width-2*PROPERTY_PADDING, slotBounds.height}; 793 | GuiDrawText("X", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 794 | val.v2.x = GuiDMSpinner(valBounds, val.v2.x, 0.0, 0.0, 1.0, PROPERTY_DECIMAL_DIGITS, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 795 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 796 | lblBounds.y = valBounds.y = slotBounds.y; 797 | GuiDrawText("Y", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 798 | val.v2.y = GuiDMSpinner(valBounds, val.v2.y, 0.0, 0.0, 1.0, PROPERTY_DECIMAL_DIGITS, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 799 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 800 | lblBounds.y = valBounds.y = slotBounds.y; 801 | if(props[p].value.vp2 != NULL) *props[p].value.vp2 = val.v2; 802 | 803 | if(props[p].type >= GUI_PROP_PVECTOR3) { 804 | GuiDrawText("Z", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 805 | val.v3.z = GuiDMSpinner(valBounds, val.v3.z, 0.0, 0.0, 1.0, PROPERTY_DECIMAL_DIGITS, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 806 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 807 | lblBounds.y = valBounds.y = slotBounds.y; 808 | if(props[p].value.vp3 != NULL) *props[p].value.vp3 = val.v3; 809 | } 810 | 811 | if(props[p].type >= GUI_PROP_PVECTOR4) { 812 | GuiDrawText("W", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 813 | val.v4.w = GuiDMSpinner(valBounds, val.v4.w, 0.0, 0.0, 1.0, PROPERTY_DECIMAL_DIGITS, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 814 | if(props[p].value.vp4 != NULL) *props[p].value.vp4 = val.v4; 815 | } 816 | 817 | 818 | } 819 | } break; 820 | 821 | case GUI_PROP_RECT: 822 | { 823 | Rectangle titleBounds = { propBounds.x, propBounds.y, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) }; 824 | // Collapse/Expand property on click 825 | if((propState == GUI_STATE_PRESSED) && CheckCollisionPointRec(mousePos, titleBounds)) 826 | PROP_TOGGLE_FLAG(&props[p], GUI_PFLAG_COLLAPSED); 827 | 828 | // draw property name 829 | GuiDrawText(PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED) ? PROPERTY_COLLAPSED_ICON : PROPERTY_EXPANDED_ICON, titleBounds, GUI_TEXT_ALIGN_LEFT, textColor); 830 | GuiDrawText(props[p].name, (Rectangle){propBounds.x+PROPERTY_ICON_SIZE+PROPERTY_PADDING, propBounds.y, propBounds.width-PROPERTY_ICON_SIZE-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 831 | GuiDrawText(TextFormat("[%.0f, %.0f, %.0f, %.0f]", props[p].value.vrect.x, props[p].value.vrect.y, props[p].value.vrect.width, props[p].value.vrect.height), 832 | (Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 2}, GUI_TEXT_ALIGN_LEFT, textColor); 833 | 834 | // draw X, Y, Width, Height values (only when expanded) 835 | if(!PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED)) { 836 | Rectangle slotBounds = { propBounds.x, propBounds.y+GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)+1, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2}; 837 | Rectangle lblBounds = { propBounds.x+PROPERTY_PADDING, slotBounds.y, GetTextWidth("Height"), slotBounds.height}; 838 | Rectangle valBounds = { lblBounds.x+lblBounds.width+PROPERTY_PADDING, slotBounds.y, propBounds.width-lblBounds.width-2*PROPERTY_PADDING, slotBounds.height}; 839 | GuiDrawText("X", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 840 | props[p].value.vrect.x = GuiDMSpinner(valBounds, props[p].value.vrect.x, 0.0, 0.0, 1.0, 0, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 841 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 842 | lblBounds.y = valBounds.y = slotBounds.y; 843 | GuiDrawText("Y", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 844 | props[p].value.vrect.y = GuiDMSpinner(valBounds, props[p].value.vrect.y, 0.0, 0.0, 1.0, 0, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 845 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 846 | lblBounds.y = valBounds.y = slotBounds.y; 847 | GuiDrawText("Width", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 848 | props[p].value.vrect.width = GuiDMSpinner(valBounds, props[p].value.vrect.width, 0.0, 0.0, 1.0, 0, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 849 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 850 | lblBounds.y = valBounds.y = slotBounds.y; 851 | GuiDrawText("Height", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 852 | props[p].value.vrect.height = GuiDMSpinner(valBounds, props[p].value.vrect.height, 0.0, 0.0, 1.0, 0, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 853 | } 854 | } break; 855 | 856 | 857 | case GUI_PROP_COLOR: { 858 | Rectangle titleBounds = { propBounds.x, propBounds.y, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) }; 859 | // Collapse/Expand property on click 860 | if((propState == GUI_STATE_PRESSED) && CheckCollisionPointRec(mousePos, titleBounds)) 861 | PROP_TOGGLE_FLAG(&props[p], GUI_PFLAG_COLLAPSED); 862 | 863 | // draw property name 864 | GuiDrawText(PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED) ? PROPERTY_COLLAPSED_ICON : PROPERTY_EXPANDED_ICON, titleBounds, GUI_TEXT_ALIGN_LEFT, textColor); 865 | GuiDrawText(props[p].name, (Rectangle){propBounds.x+PROPERTY_ICON_SIZE+PROPERTY_PADDING, propBounds.y+1, propBounds.width-PROPERTY_ICON_SIZE-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2}, GUI_TEXT_ALIGN_LEFT, textColor); 866 | DrawLineEx( (Vector2){propBounds.x+propBounds.width/2, propBounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 5}, (Vector2){propBounds.x+propBounds.width, propBounds.y + GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 5}, 6.0f, props[p].value.vcolor); 867 | const char* fmt = TextFormat("#%02X%02X%02X%02X", props[p].value.vcolor.r, props[p].value.vcolor.g, props[p].value.vcolor.b, props[p].value.vcolor.a); 868 | char clip[10] = "\0"; 869 | memcpy(clip, fmt, 10*sizeof(char)); // copy to temporary buffer since we can't be sure when TextFormat() will be called again and our text will be overwritten 870 | GuiDrawText(fmt, (Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 2}, GUI_TEXT_ALIGN_LEFT, textColor); 871 | 872 | // draw R, G, B, A values (only when expanded) 873 | if(!PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED)) { 874 | Rectangle slotBounds = { propBounds.x, propBounds.y+GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)+1, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2}; 875 | Rectangle lblBounds = { propBounds.x+PROPERTY_PADDING, slotBounds.y, GetTextWidth("A"), slotBounds.height}; 876 | Rectangle valBounds = { lblBounds.x+lblBounds.width+PROPERTY_PADDING, slotBounds.y, GetTextWidth("000000"), slotBounds.height}; 877 | Rectangle sbarBounds = { valBounds.x + valBounds.width + PROPERTY_PADDING, slotBounds.y, slotBounds.width - 3*PROPERTY_PADDING - lblBounds.width - valBounds.width, slotBounds.height }; 878 | 879 | if(sbarBounds.width <= GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2) valBounds.width = propBounds.width-lblBounds.width-2*PROPERTY_PADDING; // hide slider when no space 880 | // save current scrollbar style 881 | int tmpSliderPadding = GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING); 882 | int tmpPadding = GuiGetStyle(SCROLLBAR, SCROLL_PADDING); 883 | int tmpBorder = GuiGetStyle(SCROLLBAR, BORDER_WIDTH); 884 | int tmpSliderSize = GuiGetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE); 885 | int tmpArrows = GuiGetStyle(SCROLLBAR, ARROWS_VISIBLE); 886 | Color tmpBG1 = GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_DISABLED)); 887 | // set a custom scrollbar style 888 | GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING, 3); 889 | GuiSetStyle(SCROLLBAR, SCROLL_PADDING, 10); 890 | GuiSetStyle(SCROLLBAR, BORDER_WIDTH, 0); 891 | GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, 6); 892 | GuiSetStyle(SCROLLBAR, ARROWS_VISIBLE, 0); 893 | GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, GuiGetStyle(DEFAULT, BACKGROUND_COLOR)); // disable scrollbar background 894 | 895 | GuiDrawText("R", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 896 | props[p].value.vcolor.r = GuiDMValueBox(valBounds, props[p].value.vcolor.r, 0.0, 255.0, 0, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 897 | if(sbarBounds.width > GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2) 898 | props[p].value.vcolor.r = GuiScrollBar(sbarBounds, props[p].value.vcolor.r, 0, 255); 899 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 900 | lblBounds.y = valBounds.y = sbarBounds.y = slotBounds.y; 901 | 902 | GuiDrawText("G", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 903 | props[p].value.vcolor.g = GuiDMValueBox(valBounds, props[p].value.vcolor.g, 0.0, 255.0, 0, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 904 | if(sbarBounds.width > GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2) 905 | props[p].value.vcolor.g = GuiScrollBar(sbarBounds, props[p].value.vcolor.g, 0, 255); 906 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 907 | lblBounds.y = valBounds.y = sbarBounds.y = slotBounds.y; 908 | 909 | GuiDrawText("B", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 910 | props[p].value.vcolor.b = GuiDMValueBox(valBounds, props[p].value.vcolor.b, 0.0, 255.0, 0, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 911 | if(sbarBounds.width > GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2) 912 | props[p].value.vcolor.b = GuiScrollBar(sbarBounds, props[p].value.vcolor.b, 0, 255); 913 | slotBounds.y += GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT); 914 | lblBounds.y = valBounds.y = sbarBounds.y = slotBounds.y; 915 | 916 | GuiDrawText("A", lblBounds, GUI_TEXT_ALIGN_LEFT, textColor); 917 | props[p].value.vcolor.a = GuiDMValueBox(valBounds, props[p].value.vcolor.a, 0.0, 255.0, 0, (propState == GUI_STATE_FOCUSED) && CheckCollisionPointRec(mousePos, slotBounds) ); 918 | if(sbarBounds.width > GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)-2) 919 | props[p].value.vcolor.a = GuiScrollBar(sbarBounds, props[p].value.vcolor.a, 0, 255); 920 | 921 | // load saved scrollbar style 922 | GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_PADDING, tmpSliderPadding); 923 | GuiSetStyle(SCROLLBAR, SCROLL_PADDING, tmpPadding); 924 | GuiSetStyle(SCROLLBAR, BORDER_WIDTH, tmpBorder); 925 | GuiSetStyle(SCROLLBAR, SCROLL_SLIDER_SIZE, tmpSliderSize); 926 | GuiSetStyle(SCROLLBAR, ARROWS_VISIBLE, tmpArrows); 927 | GuiSetStyle(DEFAULT, BORDER_COLOR_DISABLED, ColorToInt(tmpBG1)); 928 | } 929 | 930 | // support COPY/PASTE (need to do this here since GuiDMValueBox() also has COPY/PASTE so we need to overwrite it) 931 | if((propState == GUI_STATE_FOCUSED)) { 932 | if(IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_C)) 933 | SetClipboardText(clip); 934 | else if(IsKeyDown(KEY_LEFT_CONTROL) && IsKeyPressed(KEY_V)){ 935 | unsigned int a = props[p].value.vcolor.a, r = props[p].value.vcolor.r, g=props[p].value.vcolor.g, b=props[p].value.vcolor.b; 936 | sscanf(GetClipboardText(), "#%02X%02X%02X%02X", &r, &g, &b, &a); 937 | props[p].value.vcolor.r=r; props[p].value.vcolor.g=g; props[p].value.vcolor.b=b; props[p].value.vcolor.a=a; 938 | } 939 | } 940 | } break; 941 | 942 | case GUI_PROP_SECTION: { 943 | Rectangle titleBounds = { propBounds.x, propBounds.y, propBounds.width, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) }; 944 | // Collapse/Expand section on click 945 | if( (propState == GUI_STATE_PRESSED) && CheckCollisionPointRec(mousePos, titleBounds) ) 946 | PROP_TOGGLE_FLAG(&props[p], GUI_PFLAG_COLLAPSED); 947 | 948 | if(!PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED)) { 949 | GuiDrawText(PROPERTY_EXPANDED_ICON, titleBounds, GUI_TEXT_ALIGN_LEFT, textColor); 950 | GuiDrawText(props[p].name, (Rectangle){propBounds.x+PROPERTY_ICON_SIZE+PROPERTY_PADDING, propBounds.y, propBounds.width-PROPERTY_ICON_SIZE-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_CENTER, textColor); 951 | } else { 952 | GuiDrawText(PROPERTY_COLLAPSED_ICON, titleBounds, GUI_TEXT_ALIGN_LEFT, textColor); 953 | GuiDrawText(TextFormat("%s [%i]", props[p].name, props[p].value.vsection), (Rectangle){propBounds.x+PROPERTY_ICON_SIZE+PROPERTY_PADDING, propBounds.y, propBounds.width-PROPERTY_ICON_SIZE-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_CENTER, textColor); 954 | } 955 | } break; 956 | 957 | 958 | // NOTE: Add your custom property here !! 959 | default: { 960 | // draw property name 961 | GuiDrawText(props[p].name, (Rectangle){propBounds.x + PROPERTY_PADDING, propBounds.y, propBounds.width/2-PROPERTY_PADDING, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT)}, GUI_TEXT_ALIGN_LEFT, textColor); 962 | // draw property type 963 | GuiDrawText(TextFormat("TYPE %i", props[p].type), (Rectangle){propBounds.x+propBounds.width/2, propBounds.y + 1, propBounds.width/2, GuiGetStyle(LISTVIEW, LIST_ITEMS_HEIGHT) - 2}, GUI_TEXT_ALIGN_LEFT, textColor); 964 | } break; 965 | 966 | } // end of switch() 967 | 968 | GuiSetState(state); 969 | } 970 | 971 | currentHeight += height + 1; 972 | 973 | // Skip collapsed section. Don't put this code inside the switch !! 974 | if(props[p].type == GUI_PROP_SECTION && (PROP_CHECK_FLAG(&props[p], GUI_PFLAG_COLLAPSED))) p += props[p].value.vsection; 975 | } // end for 976 | EndScissorMode(); 977 | 978 | if(useScrollBar) { 979 | scroll = -GuiScrollBar(scrollBarBounds, -scroll, 0, maxScroll); 980 | *scrollIndex = scroll; 981 | } 982 | //-------------------------------------------------------------------- 983 | 984 | if(focus != NULL) *focus = propFocused; 985 | } 986 | 987 | 988 | 989 | bool GuiDMSaveProperties(const char* file, GuiDMProperty* props, int count) { 990 | if(file == NULL || props == NULL) return false; 991 | if(count == 0) return true; 992 | 993 | FILE* f = fopen(file, "w"); 994 | if(f == NULL) return false; 995 | 996 | // write header 997 | fprintf(f, "#\n# Property types:\n" 998 | "# b // Bool\n" 999 | "# i // Int\n" 1000 | "# f // Float\n" 1001 | "# t // Text\n" 1002 | "# l // Select\n" 1003 | "# g // Section (Group)\n" 1004 | "# v2 // Vector 2D\n" 1005 | "# v3 // Vector 3D\n" 1006 | "# v4 // Vector 4D\n" 1007 | "# r // Rectangle\n" 1008 | "# c // Color\n" 1009 | "#\n\n"); 1010 | for(int p=0; p 19 | #include 20 | 21 | #define LIB_RAY_PARTICLES_IMPL 22 | #include "particles.h" 23 | #include "global.h" 24 | 25 | #define SCREEN_WIDTH 960 26 | #define SCREEN_HEIGHT 540 27 | 28 | 29 | static void UpdateEditor(); 30 | static void DrawEditor(); 31 | 32 | inline void SetDefaultOptions() 33 | { 34 | // set default editor colors 35 | Editor.options.bg = RAYWHITE; 36 | Editor.options.fg = BLACK; 37 | Editor.options.gridcolor = LIGHTGRAY; 38 | Editor.options.debug = RED; 39 | 40 | Editor.options.show_debug = Editor.options.show_grid = true; 41 | } 42 | 43 | void InitializeEditor() 44 | { 45 | //initialize raylib 46 | SetConfigFlags(FLAG_WINDOW_RESIZABLE); 47 | InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Particle Editor (v" EDITOR_VER ")"); 48 | SetWindowMinSize(800, 450); 49 | SetTargetFPS(60); 50 | 51 | // initialize the global editor variable 52 | Editor = (struct SEditor){0}; 53 | Editor.camera.zoom = 1.0f; 54 | Editor.active_emitter = -1; // set no emitter active 55 | 56 | // setup default options 57 | SetDefaultOptions(); 58 | 59 | // load options from storage 60 | SetTraceLogLevel(LOG_NONE); 61 | int saved = LoadStorageValue(7); 62 | if(saved) 63 | { 64 | Editor.options.show_grid = LoadStorageValue(0); 65 | Editor.options.show_placeholder = LoadStorageValue(1); 66 | Editor.options.show_debug = LoadStorageValue(2); 67 | Editor.options.bg = GetColor(LoadStorageValue(3)); 68 | Editor.options.fg = GetColor(LoadStorageValue(4)); 69 | Editor.options.gridcolor = GetColor(LoadStorageValue(5)); 70 | Editor.options.debug = GetColor(LoadStorageValue(6)); 71 | } 72 | SetTraceLogLevel(LOG_INFO); 73 | 74 | // set default ids 75 | for(int i=0; iparticles.data); 87 | free(Editor.emitters[i]->config.gradient.colors); 88 | free(Editor.emitters[i]->config.forces.data); 89 | free(Editor.emitters[i]); 90 | Editor.statistics.total_mem -= sizeof(Emitter) + MAX_PARTICLES*sizeof(Particle) + MAX_COLORS*sizeof(Color) + MAX_FORCES*sizeof(Force); 91 | Editor.emitters[i] = NULL; 92 | } 93 | } 94 | 95 | void FinalizeEditor() 96 | { 97 | // unload textures 98 | UnloadTexture(Editor.placeholder); 99 | if(Editor.clipboard != NULL) UnloadTexture(Editor.clipboard->config.atlas.texture); 100 | 101 | // deallocate emitters and clipboard 102 | DeallocateEmitters(); 103 | if(Editor.clipboard != NULL) free(Editor.clipboard); 104 | 105 | // save options 106 | SetTraceLogLevel(LOG_NONE); 107 | SaveStorageValue(0, Editor.options.show_grid); 108 | SaveStorageValue(1, Editor.options.show_placeholder); 109 | SaveStorageValue(2, Editor.options.show_debug); 110 | SaveStorageValue(3, ColorToInt(Editor.options.bg)); 111 | SaveStorageValue(4, ColorToInt(Editor.options.fg)); 112 | SaveStorageValue(5, ColorToInt(Editor.options.gridcolor)); 113 | SaveStorageValue(6, ColorToInt(Editor.options.debug)); 114 | SaveStorageValue(7, 1); // when loading signals that the options were saved 115 | SetTraceLogLevel(LOG_INFO); 116 | 117 | // raylib finalize 118 | CloseWindow(); 119 | } 120 | 121 | void UpdateEditor() 122 | { 123 | // --------------------------------------------------------------------------------------- 124 | // Move camera with right mouse button and move the active emitter with the left 125 | // --------------------------------------------------------------------------------------- 126 | if(IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) 127 | { 128 | Editor.mouse = GetMousePosition(); 129 | Editor.offset = Editor.camera.offset; 130 | } 131 | else if(IsMouseButtonDown(MOUSE_RIGHT_BUTTON)) 132 | { 133 | Editor.camera.offset = Vector2Subtract(Editor.offset, Vector2Subtract(Editor.mouse, GetMousePosition())); 134 | } 135 | else if(Editor.active_emitter != -1 && CanMoveEmitter()) 136 | { 137 | if(IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { 138 | Editor.mouse = GetMousePosition(); 139 | Editor.offset = Editor.emitters[Editor.active_emitter]->position; 140 | } else if(IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { 141 | Editor.emitters[Editor.active_emitter]->position = Vector2Subtract(Editor.offset, Vector2Subtract(Editor.mouse, GetMousePosition())); 142 | } 143 | } 144 | 145 | // --------------------------------------------------------------------------------------- 146 | // Handle Copy/Pasting of emitters 147 | // --------------------------------------------------------------------------------------- 148 | if(Editor.active_emitter != -1 && CanMoveEmitter() && (IsKeyDown(KEY_RIGHT_CONTROL) || IsKeyDown(KEY_LEFT_CONTROL)) ) 149 | { 150 | if(IsKeyPressed(KEY_C)) 151 | { 152 | if(Editor.clipboard != NULL) 153 | { 154 | // erase old clipboard content 155 | memset(Editor.clipboard->particles.data, 0, MAX_PARTICLES*sizeof(Particle)); 156 | memset(Editor.clipboard->config.gradient.colors, 0, MAX_COLORS*sizeof(Color)); 157 | memset(Editor.clipboard->config.forces.data, 0, MAX_FORCES*sizeof(Force)); 158 | 159 | // unload texture only if not in use 160 | bool unload = true; 161 | for(int i=0; iconfig.atlas.texture.id == Editor.clipboard->config.atlas.texture.id) { 164 | unload = false; 165 | break; 166 | } 167 | } 168 | 169 | if(unload) UnloadTexture(Editor.clipboard->config.atlas.texture); 170 | } 171 | else 172 | { 173 | // allocate clipboard memory 174 | Editor.clipboard = calloc(1, sizeof*Editor.clipboard); 175 | if(Editor.clipboard == NULL) TraceLog(LOG_FATAL, "CLIPBOARD: Failed to allocate memory"); 176 | 177 | Editor.clipboard->particles.data = calloc(MAX_PARTICLES, sizeof(Particle)); 178 | if(Editor.clipboard->particles.data == NULL) TraceLog(LOG_FATAL, "CLIPBOARD: Failed to allocate memory"); 179 | 180 | Editor.clipboard->config.gradient.colors = calloc(MAX_COLORS, sizeof(Color)); 181 | if(Editor.clipboard->config.gradient.colors == NULL) TraceLog(LOG_FATAL, "CLIPBOARD: Failed to allocate memory"); 182 | 183 | Editor.clipboard->config.forces.data = calloc(MAX_FORCES, sizeof(Force)); 184 | if(Editor.clipboard->config.forces.data == NULL) TraceLog(LOG_FATAL, "CLIPBOARD: Failed to allocate memory"); 185 | } 186 | 187 | // save clipboard pointers 188 | Particle* particles = Editor.clipboard->particles.data; 189 | Color* colors = Editor.clipboard->config.gradient.colors; 190 | Force* forces = Editor.clipboard->config.forces.data; 191 | 192 | // overwrite clipboard 193 | *Editor.clipboard = *Editor.emitters[Editor.active_emitter]; 194 | memcpy(forces, Editor.emitters[Editor.active_emitter]->config.forces.data, MAX_FORCES*sizeof(Force)); 195 | memcpy(colors, Editor.emitters[Editor.active_emitter]->config.gradient.colors, MAX_COLORS*sizeof(Color)); 196 | 197 | // restore the actual clipboard pointers 198 | Editor.clipboard->particles.data = particles; 199 | Editor.clipboard->config.gradient.colors = colors; 200 | Editor.clipboard->config.forces.data = forces; 201 | 202 | Editor.clipboard->emit_timer = Editor.clipboard->spawn_timer = 0.0f; 203 | Editor.clipboard->particles.count = 1; 204 | } 205 | else if(IsKeyPressed(KEY_V)) 206 | { 207 | if(Editor.clipboard != NULL) 208 | { 209 | // erase emitter content 210 | memset(Editor.emitters[Editor.active_emitter]->particles.data, 0, MAX_PARTICLES*sizeof(Particle)); 211 | memset(Editor.emitters[Editor.active_emitter]->config.gradient.colors, 0, MAX_COLORS*sizeof(Color)); 212 | memset(Editor.emitters[Editor.active_emitter]->config.forces.data, 0, MAX_FORCES*sizeof(Force)); 213 | 214 | // unload texture only if not in use 215 | bool unload = true; 216 | for(int i=0; iconfig.atlas.texture.id == Editor.emitters[Editor.active_emitter]->config.atlas.texture.id) { 219 | unload = false; 220 | break; 221 | } 222 | } 223 | 224 | // also check the clipboard texture 225 | if(Editor.emitters[Editor.active_emitter]->config.atlas.texture.id == Editor.clipboard->config.atlas.texture.id) { 226 | unload = false; 227 | } 228 | 229 | if(unload) UnloadTexture(Editor.emitters[Editor.active_emitter]->config.atlas.texture); 230 | 231 | // backup pointers 232 | Particle* particles = Editor.emitters[Editor.active_emitter]->particles.data; 233 | Color* colors = Editor.emitters[Editor.active_emitter]->config.gradient.colors; 234 | Force* forces = Editor.emitters[Editor.active_emitter]->config.forces.data; 235 | 236 | // overwrite emitter with the clipboard data 237 | *Editor.emitters[Editor.active_emitter] = *Editor.clipboard; 238 | memcpy(forces, Editor.clipboard->config.forces.data, MAX_FORCES*sizeof(Force)); 239 | memcpy(colors, Editor.clipboard->config.gradient.colors, MAX_COLORS*sizeof(Color)); 240 | 241 | // restore emitter pointers 242 | Editor.emitters[Editor.active_emitter]->particles.data = particles; 243 | Editor.emitters[Editor.active_emitter]->config.gradient.colors = colors; 244 | Editor.emitters[Editor.active_emitter]->config.forces.data = forces; 245 | } 246 | } 247 | } 248 | 249 | // --------------------------------------------------------------------------------------- 250 | // Handle dropped files 251 | // --------------------------------------------------------------------------------------- 252 | if(IsFileDropped()) 253 | { 254 | char** file = NULL; 255 | int count = 0; 256 | file = GetDroppedFiles(&count); 257 | if(IsFileExtension(file[0], ".png")) 258 | { 259 | // user dropped a texture 260 | if(Editor.active_emitter != -1) 261 | { 262 | Texture t = LoadTexture(file[0]); 263 | if(t.id > 0) 264 | { 265 | bool unload = true; 266 | // only unload if no other emitter has the texture in use 267 | for(int i=0; iconfig.atlas.texture.id == Editor.emitters[Editor.active_emitter]->config.atlas.texture.id) { 270 | unload = false; 271 | break; 272 | } 273 | } 274 | 275 | // check for clipboard texture 276 | if(Editor.clipboard != NULL && Editor.emitters[Editor.active_emitter]->config.atlas.texture.id == Editor.clipboard->config.atlas.texture.id) { 277 | unload = false; 278 | } 279 | 280 | if(unload) UnloadTexture(Editor.emitters[Editor.active_emitter]->config.atlas.texture); 281 | 282 | Editor.emitters[Editor.active_emitter]->config.atlas.texture = t; 283 | Editor.emitters[Editor.active_emitter]->config.atlas.hframes = Editor.emitters[Editor.active_emitter]->config.atlas.vframes = 0; 284 | Editor.active_window = 2; // switch to texture window 285 | } 286 | } 287 | else 288 | { 289 | // dropped placeholder 290 | Texture t = LoadTexture(file[0]); 291 | if(t.id > 0) { 292 | UnloadTexture(Editor.placeholder); 293 | Editor.placeholder = t; 294 | } 295 | } 296 | } 297 | else if(IsFileExtension(file[0], ".dps")) { 298 | if(!LoadEmitters(file[0])) TraceLog(LOG_WARNING, "Failed to load emitters"); 299 | } 300 | ClearDroppedFiles(); 301 | } 302 | 303 | 304 | // --------------------------------------------------------------------------------------- 305 | // Update emitters 306 | // --------------------------------------------------------------------------------------- 307 | Editor.statistics.updated = 0; 308 | for(int i=0; iposition; 365 | DrawCircleLines(pos.x, pos.y, 4.0f, Editor.options.debug); 366 | 367 | // draw the timers 368 | if(Editor.emitters[i]->life > 0.0f) { 369 | if(Editor.emitters[i]->delay > 0.0f && Editor.emitters[i]->emit_timer < Editor.emitters[i]->delay) { 370 | DrawRectangleLines(pos.x - 4, pos.y + 6, 32, 4, Editor.options.debug); 371 | DrawRectangle(pos.x - 4, pos.y + 7, 32.0f*Editor.emitters[i]->emit_timer/Editor.emitters[i]->delay, 3, Editor.options.debug); 372 | DrawText(TextFormat("s %.2f", Editor.emitters[i]->emit_timer), pos.x - 4, pos.y + 12, 10, Editor.options.debug); 373 | } 374 | else if(Editor.emitters[i]->delay > 0.0f && Editor.emitters[i]->emit_timer > Editor.emitters[i]->delay + Editor.emitters[i]->life && 375 | Editor.emitters[i]->emit_timer < 2*Editor.emitters[i]->delay + Editor.emitters[i]->life) 376 | { 377 | float time = Editor.emitters[i]->emit_timer - Editor.emitters[i]->delay - Editor.emitters[i]->life; 378 | DrawRectangleLines(pos.x - 4, pos.y + 6, 32, 4, Editor.options.debug); 379 | DrawRectangle(pos.x - 4, pos.y + 7, 32.0f*time/Editor.emitters[i]->delay, 2, Editor.options.debug); 380 | DrawText(TextFormat("e %.2f", time), pos.x - 4, pos.y + 12, 10, Editor.options.debug); 381 | } 382 | } 383 | 384 | if(Editor.emitters[i]->config.container.type == EMITTER_CIRCLE) 385 | DrawCircleLines(pos.x, pos.y, Editor.emitters[i]->config.container.opt1, Editor.options.debug); 386 | else if(Editor.emitters[i]->config.container.type == EMITTER_RING) 387 | DrawRingLines(pos, Editor.emitters[i]->config.container.opt1, Editor.emitters[i]->config.container.opt2, 0.0f, 360.0f, 0, Editor.options.debug); 388 | else if(Editor.emitters[i]->config.container.type == EMITTER_RECT) 389 | { 390 | const float width = Editor.emitters[i]->config.container.opt1; 391 | const float height = Editor.emitters[i]->config.container.opt2; 392 | DrawRectangleLines(pos.x-width/2, pos.y-height/2, width, height, Editor.options.debug); 393 | } 394 | } 395 | } 396 | 397 | Editor.statistics.drawn = params.drawn; 398 | Editor.statistics.pixels = params.pixels; 399 | EndMode2D(); 400 | } 401 | 402 | // Generates a nice color with a random hue 403 | inline Color GenerateRandomColor(float s, float v) 404 | { 405 | const float Phi = 0.618033988749895; // golden ratio conjugate 406 | float h = GetRandomValue(0, 360); 407 | h = fmodf((h + h*Phi), 360.0f); 408 | return ColorFromHSV(h, s, v); 409 | } 410 | 411 | static inline void SetDefaultEmitterConfig(Emitter* e) 412 | { 413 | e->config.emission = 10; 414 | e->config.pulses = 0; 415 | e->config.size.min = 1.0f; 416 | e->config.size.max = 2.0f; 417 | e->config.scale.start = e->config.scale.end = 1.0f; 418 | e->config.age.min = e->config.age.max = 4.0f; 419 | e->config.speed.min = e->config.speed.max = 80.0f; 420 | e->config.angle.max = 360.0f; 421 | e->config.easing = &EaseLinearNone; 422 | e->delay = 0.0f; 423 | e->life = 4.0f; 424 | } 425 | 426 | void EditorAddEmitter(Vector2 loc) 427 | { 428 | if(Editor.emitter_count < MAX_EMITTERS) 429 | { 430 | int pos = Editor.emitter_count; 431 | 432 | // allocate memory for emitter 433 | if(Editor.emitters[pos] == NULL) { 434 | Editor.emitters[pos] = calloc(1, sizeof(Emitter)); 435 | if(Editor.emitters[pos] == NULL) TraceLog(LOG_FATAL, "EMITTER: Failed to allocate memory"); 436 | Editor.statistics.total_mem += sizeof(Emitter); 437 | } 438 | 439 | *Editor.emitters[pos] = (Emitter){0}; 440 | // set default configuration for the new emitter 441 | SetDefaultEmitterConfig(Editor.emitters[pos]); 442 | 443 | // allocate memory for emitter particles 444 | if(Editor.emitters[pos]->particles.data == NULL) { 445 | Editor.emitters[pos]->particles.data = calloc(MAX_PARTICLES, sizeof(Particle)); 446 | if(Editor.emitters[pos]->particles.data == NULL) TraceLog(LOG_FATAL, "PARTICLES: Failed to allocate memory"); 447 | Editor.emitters[pos]->particles.max = MAX_PARTICLES; 448 | Editor.statistics.total_mem += MAX_PARTICLES*sizeof(Particle); 449 | } 450 | Editor.emitters[pos]->particles.count = 0; 451 | 452 | // allocate memory for the colors 453 | if(Editor.emitters[pos]->config.gradient.colors == NULL) { 454 | Editor.emitters[pos]->config.gradient.colors = calloc(MAX_COLORS, sizeof(Color)); 455 | if(Editor.emitters[pos]->config.gradient.colors == NULL) TraceLog(LOG_FATAL, "COLORS: Failed to allocate memory"); 456 | Editor.statistics.total_mem += MAX_COLORS*sizeof(Color); 457 | } 458 | Editor.emitters[pos]->config.gradient.count = 1; // at least one color should always be set 459 | Editor.emitters[pos]->config.gradient.colors[0] = GenerateRandomColor(0.4f, 0.89f); 460 | 461 | // allocate memory for the forces 462 | if(Editor.emitters[pos]->config.forces.data == NULL) { 463 | Editor.emitters[pos]->config.forces.data = calloc(MAX_FORCES, sizeof(Force)); 464 | if(Editor.emitters[pos]->config.forces.data == NULL) TraceLog(LOG_FATAL, "FORCES: Failed to allocate memory"); 465 | Editor.statistics.total_mem += MAX_FORCES*sizeof(Force); 466 | } 467 | Editor.emitters[pos]->config.forces.count = 0; 468 | 469 | Editor.emitters[pos]->position = loc; 470 | 471 | Editor.emitter_count += 1; 472 | } 473 | } 474 | 475 | void EditorRemoveEmitter() 476 | { 477 | if(Editor.emitter_count > 0) 478 | { 479 | int pos = Editor.emitter_count - 1; 480 | if(Editor.active_emitter == pos) Editor.active_emitter = pos - 1; 481 | 482 | free(Editor.emitters[pos]->particles.data); 483 | free(Editor.emitters[pos]->config.gradient.colors); 484 | free(Editor.emitters[pos]->config.forces.data); 485 | 486 | if(Editor.clipboard == NULL || (Editor.clipboard != NULL && Editor.clipboard->config.atlas.texture.id != Editor.emitters[pos]->config.atlas.texture.id)) 487 | UnloadTexture(Editor.emitters[pos]->config.atlas.texture); 488 | 489 | free(Editor.emitters[pos]); 490 | Editor.emitters[pos] = NULL; 491 | 492 | Editor.emitter_count -= 1; 493 | Editor.statistics.total_mem -= sizeof(Emitter) + MAX_PARTICLES*sizeof(Particle) + MAX_COLORS*sizeof(Color) + MAX_FORCES*sizeof(Force); 494 | } 495 | } 496 | 497 | void EditorMoveUpEmitter() 498 | { 499 | if(Editor.active_emitter > 0) 500 | { 501 | // swap current active emitter with the one above it 502 | Emitter* tmp = Editor.emitters[Editor.active_emitter]; 503 | Editor.emitters[Editor.active_emitter] = Editor.emitters[Editor.active_emitter - 1]; 504 | Editor.emitters[Editor.active_emitter - 1] = tmp; 505 | 506 | //do the same with the ids 507 | int tid = Editor.emitter_id[Editor.active_emitter]; 508 | Editor.emitter_id[Editor.active_emitter] = Editor.emitter_id[Editor.active_emitter - 1]; 509 | Editor.emitter_id[Editor.active_emitter - 1] = tid; 510 | Editor.active_emitter -= 1; 511 | } 512 | } 513 | 514 | void EditorMoveDownEmitter() 515 | { 516 | if(Editor.active_emitter != -1 && Editor.active_emitter + 1 < Editor.emitter_count) { 517 | // swap current active emitter with the one below it 518 | Emitter* tmp = Editor.emitters[Editor.active_emitter]; 519 | Editor.emitters[Editor.active_emitter] = Editor.emitters[Editor.active_emitter + 1]; 520 | Editor.emitters[Editor.active_emitter + 1] = tmp; 521 | 522 | //do the same with the ids 523 | int tid = Editor.emitter_id[Editor.active_emitter]; 524 | Editor.emitter_id[Editor.active_emitter] = Editor.emitter_id[Editor.active_emitter + 1]; 525 | Editor.emitter_id[Editor.active_emitter + 1] = tid; 526 | 527 | Editor.active_emitter += 1; 528 | } 529 | } 530 | 531 | void EditorSyncEmitters() 532 | { 533 | // reset all emitter particles so they are in sync 534 | for(int i=0; iparticles.data, 0, MAX_PARTICLES*sizeof(Particle)); 536 | Editor.emitters[i]->particles.count = 0; 537 | Editor.emitters[i]->spawn_timer = 0.0f; 538 | Editor.emitters[i]->emit_timer = 0.0f; 539 | } 540 | } 541 | 542 | 543 | -------------------------------------------------------------------------------- /examples/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/arrow.png -------------------------------------------------------------------------------- /examples/cloud_1.dps: -------------------------------------------------------------------------------- 1 | # 2 | # Demizdor's Particle System file (v1.03 - ALPHA) 3 | # 4 | # Emitter properties: 5 | # n name_of_particle_system 6 | # 7 | # g ID gradient_count g0 g1 ... gn 8 | # f ID forces_count f0_direction f0_strength .. fn_direction fn_strength 9 | # a ID hframes vframes loop texture 10 | # c ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end 11 | # e ID pos_x pos_y max_particles life delay easing flags type opt1 opt2 12 | # 13 | 14 | n animated_textures_test 15 | 16 | 17 | g 00 05 D5000000 D50000FF E7DF5DFF 1115FFFF 2D31FF00 18 | f 00 01 -60.63 -189.47 19 | a 00 8 8 1 cloud_1.png 20 | c 00 0211 0 0.300 1.000 200.0 360.0 10 -10 1.0000 2.0000 40.00 60.00 1.0000 1.7000 141 -283 0 -293 97.8 0.0 21 | e 00 -12 47 2000 4.0000 0.00 00 0000 256 00 0 0 22 | -------------------------------------------------------------------------------- /examples/cloud_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/cloud_1.png -------------------------------------------------------------------------------- /examples/directional_rotation_test_1.dps: -------------------------------------------------------------------------------- 1 | # 2 | # Demizdor's Particle System file (v1.03 - ALPHA) 3 | # 4 | # Emitter properties: 5 | # n name_of_particle_system 6 | # 7 | # g ID gradient_count g0 g1 ... gn 8 | # f ID forces_count f0_direction f0_strength .. fn_direction fn_strength 9 | # a ID hframes vframes loop texture 10 | # c ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end 11 | # e ID pos_x pos_y max_particles life delay easing flags type opt1 opt2 12 | # 13 | 14 | n directional_rotation_test 15 | 16 | 17 | g 00 05 88B8E200 F37BA1FF 6B6DE7FF C37FE9FF 63AFC600 18 | a 00 0 0 1 arrow.png 19 | c 00 1000 0 0.800 1.000 0.0 360.0 4 -4 3.0000 4.0000 40.00 80.00 0.1000 0.2000 67 0 33 207 180.0 180.0 20 | e 00 340 280 2000 4.0000 0.00 0000 384 01 20 80 21 | -------------------------------------------------------------------------------- /examples/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/fire.png -------------------------------------------------------------------------------- /examples/fire_1.dps: -------------------------------------------------------------------------------- 1 | # 2 | # Demizdor's Particle System file (v1.03 - ALPHA) 3 | # 4 | # Emitter properties: 5 | # n name_of_particle_system 6 | # p placeholder_texture 7 | # 8 | # g ID gradient_count g0 g1 ... gn 9 | # f ID forces_count f0_direction f0_strength .. fn_direction fn_strength 10 | # a ID hframes vframes loop texture 11 | # c ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end 12 | # e ID pos_x pos_y max_particles life delay blend_mode easing flags type opt1 opt2 13 | # 14 | 15 | n fire 16 | 17 | 18 | g 00 05 EC7100B2 EC4F38EF FFDA24D3 18000062 00000000 19 | f 00 01 90.00 9.00 20 | a 00 0 0 1 fire.png 21 | c 00 0150 0 0.200 0.400 250.0 290.0 10 -10 1.0000 2.0000 10.00 20.00 1.8000 3.0000 168 131 0 28 116.8 420.6 22 | e 00 351 367 2000 4.0000 0.00 00 0000 392 00 0 0 23 | 24 | 25 | g 01 03 3C4249AA 3C424959 3C424900 26 | c 01 0100 0 1.000 3.000 240.0 300.0 10 -10 1.0000 1.5000 20.00 30.00 1.8000 0.2000 224 9 -47 37 154.2 0.0 27 | e 01 348 373 2000 4.0000 0.00 01 0000 288 00 0 0 28 | 29 | 30 | g 02 03 FF390D3F FF90381C 322F1C00 31 | f 02 01 90.00 9.00 32 | a 02 0 0 1 fire.png 33 | c 02 0101 0 0.100 0.200 250.0 290.0 10 -10 1.0000 2.0000 10.00 20.00 1.9000 1.2000 168 37 0 28 116.8 420.6 34 | e 02 350 380 2000 4.0000 0.00 01 0001 392 00 0 0 35 | -------------------------------------------------------------------------------- /examples/multitexture_test.dps: -------------------------------------------------------------------------------- 1 | # 2 | # Demizdor's Particle System file (v1.03 - ALPHA) 3 | # 4 | # Emitter properties: 5 | # n name_of_particle_system 6 | # p placeholder_texture 7 | # 8 | # g ID gradient_count g0 g1 ... gn 9 | # f ID forces_count f0_direction f0_strength .. fn_direction fn_strength 10 | # a ID hframes vframes loop texture 11 | # c ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end 12 | # e ID pos_x pos_y max_particles life delay blend_mode easing flags type opt1 opt2 13 | # 14 | 15 | n multitexture_test 16 | 17 | 18 | g 00 03 C34EF6FF 7C5BE4FF 0F0B5C00 19 | a 00 2 2 1 multitexture_test_00.png 20 | c 00 0030 30 1.000 1.000 0.0 360.0 100 -100 4.0000 5.0000 20.00 40.00 1.0000 0.0000 0 -93 121 0 0.0 60.7 21 | e 00 355 270 2000 5.0000 0.00 00 0008 772 02 191 115 22 | -------------------------------------------------------------------------------- /examples/multitexture_test_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/multitexture_test_00.png -------------------------------------------------------------------------------- /examples/portal_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/portal_01.png -------------------------------------------------------------------------------- /examples/portal_1.dps: -------------------------------------------------------------------------------- 1 | # 2 | # Demizdor's Particle System file (v1.03 - ALPHA) 3 | # 4 | # Emitter properties: 5 | # n name_of_particle_system 6 | # p placeholder_texture 7 | # 8 | # g ID gradient_count g0 g1 ... gn 9 | # f ID forces_count f0_direction f0_strength .. fn_direction fn_strength 10 | # a ID hframes vframes loop texture 11 | # c ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end 12 | # e ID pos_x pos_y max_particles life delay easing flags type opt1 opt2 13 | # 14 | 15 | n portal 16 | 17 | 18 | g 00 05 71869FFF CC342F6E 71869FC7 71869FFF 71869F00 19 | c 00 0100 0 1.000 2.000 0.0 360.0 0 0 2.0000 3.0000 80.00 90.00 0.0000 3.5514 8 -65 0 206 140.2 327.1 20 | e 00 343 247 2000 2.0000 0.00 0000 320 00 0 0 21 | 22 | 23 | g 01 04 A06E6C76 419698FF 9B658383 1C89987C 24 | a 01 5 3 1 portal_01.png 25 | c 01 0020 0 1.100 1.500 270.0 290.0 10 -10 3.0000 4.9000 80.00 100.00 0.4000 0.6000 -252 -47 -411 112 42.1 350.5 26 | e 01 332 239 2000 3.8000 0.00 0017 460 00 20 134 27 | -------------------------------------------------------------------------------- /examples/rect_test.dps: -------------------------------------------------------------------------------- 1 | # 2 | # Demizdor's Particle System file (v1.03 - ALPHA) 3 | # 4 | # Emitter properties: 5 | # n name_of_particle_system 6 | # 7 | # g ID gradient_count g0 g1 ... gn 8 | # f ID forces_count f0_direction f0_strength .. fn_direction fn_strength 9 | # a ID hframes vframes loop texture 10 | # c ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end 11 | # e ID pos_x pos_y max_particles life delay easing flags type opt1 opt2 12 | # 13 | 14 | n emitter_rect_test 15 | 16 | 17 | g 00 07 88BDE200 9B9FD5FF A97FA1FF DD8F91FF F57779FF E3D92DFF E9E70505 18 | c 00 1000 0 1.000 4.000 0.0 180.0 -20 20 2.0000 4.0000 0.00 0.00 3.1522 0.9239 -98 -54 -54 0 114.1 337.0 19 | e 00 340 280 2000 4.0000 0.00 0006 256 01 400 200 20 | -------------------------------------------------------------------------------- /examples/ring_1.dps: -------------------------------------------------------------------------------- 1 | # 2 | # Demizdor's Particle System file (v1.03 - ALPHA) 3 | # 4 | # Emitter properties: 5 | # n name_of_particle_system 6 | # 7 | # g ID gradient_count g0 g1 ... gn 8 | # f ID forces_count f0_direction f0_strength .. fn_direction fn_strength 9 | # a ID hframes vframes loop texture 10 | # c ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end 11 | # e ID pos_x pos_y max_particles life delay easing flags type opt1 opt2 12 | # 13 | 14 | n emitter_ring_test 15 | 16 | 17 | g 00 03 DF88E200 DF88E2FF DF88E200 18 | c 00 1000 0 1.000 10.000 0.0 360.0 0 0 0.5000 1.0000 0.00 20.00 1.0000 0.2000 109 0 109 -109 0.0 0.0 19 | e 00 335 288 2000 4.0000 0.00 0004 256 03 80 100 20 | -------------------------------------------------------------------------------- /examples/smokey_donut.dps: -------------------------------------------------------------------------------- 1 | # 2 | # Demizdor's Particle System file (v1.03 - ALPHA) 3 | # 4 | # Emitter properties: 5 | # n name_of_particle_system 6 | # 7 | # g ID gradient_count g0 g1 ... gn 8 | # f ID forces_count f0_direction f0_strength .. fn_direction fn_strength 9 | # a ID hframes vframes loop texture 10 | # c ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end 11 | # e ID pos_x pos_y max_particles life delay easing flags type opt1 opt2 12 | # 13 | 14 | n smokey_donut 15 | 16 | 17 | g 00 06 888BF700 88ADD1FF BB7FC19F A993C7FF 7B93E5C1 A993C700 18 | a 00 8 8 2 cloud_1.png 19 | c 00 0201 0 1.000 1.500 0.0 360.0 0 0 4.0000 5.0000 30.00 60.00 1.0000 1.5217 0 -109 -109 -87 0.0 130.4 20 | e 00 325 262 2000 4.0000 0.00 0000 264 03 200 120 21 | -------------------------------------------------------------------------------- /examples/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/snow.png -------------------------------------------------------------------------------- /examples/snowing.dps: -------------------------------------------------------------------------------- 1 | # 2 | # Demizdor's Particle System file (v1.03 - ALPHA) 3 | # 4 | # Emitter properties: 5 | # n name_of_particle_system 6 | # 7 | # g ID gradient_count g0 g1 ... gn 8 | # f ID forces_count f0_direction f0_strength .. fn_direction fn_strength 9 | # a ID hframes vframes loop texture 10 | # c ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end 11 | # e ID pos_x pos_y max_particles life delay easing flags type opt1 opt2 12 | # 13 | 14 | n snowing 15 | 16 | g 00 03 97C5FF81 81ABFFFF 7FC1FF00 17 | a 00 0 0 1 snow.png 18 | c 00 0999 0 0.000 0.100 90.0 90.0 0 0 6.0000 8.0000 60.00 80.00 1.0000 1.3000 87 20 10 -65 0.0 173.9 19 | e 00 320 -6 2000 4.0000 0.00 0008 268 01 779 22 20 | -------------------------------------------------------------------------------- /examples/summoning_animation_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/summoning_animation_00.png -------------------------------------------------------------------------------- /examples/summoning_animation_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/summoning_animation_01.png -------------------------------------------------------------------------------- /examples/summoning_animation_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/summoning_animation_02.png -------------------------------------------------------------------------------- /examples/summoning_animation_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/summoning_animation_03.png -------------------------------------------------------------------------------- /examples/summoning_animation_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/examples/summoning_animation_04.png -------------------------------------------------------------------------------- /examples/summoning_animation_1.dps: -------------------------------------------------------------------------------- 1 | # 2 | # Demizdor's Particle System file (v1.03 - ALPHA) 3 | # 4 | # Emitter properties: 5 | # n name_of_particle_system 6 | # p placeholder_texture 7 | # 8 | # g ID gradient_count g0 g1 ... gn 9 | # f ID forces_count f0_direction f0_strength .. fn_direction fn_strength 10 | # a ID hframes vframes loop texture 11 | # c ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end 12 | # e ID pos_x pos_y max_particles life delay blend_mode easing flags type opt1 opt2 13 | # 14 | 15 | n summoning_animation 16 | 17 | 18 | g 00 02 08822BE6 E2D88800 19 | a 00 0 0 1 summoning_animation_00.png 20 | c 00 0001 1 1.000 1.000 0.0 360.0 0 0 0.5000 1.0000 0.00 0.00 0.0000 1.0000 0 0 0 0 0.0 191.6 21 | e 00 355 270 2000 0.5000 2.00 00 0013 256 00 0 0 22 | 23 | 24 | g 01 01 A5E288FF 25 | a 01 0 0 1 summoning_animation_01.png 26 | c 01 0050 1 1.000 1.000 0.0 360.0 10 -10 0.5000 1.0000 40.00 50.00 0.5000 0.0000 112 0 28 140 32.7 88.8 27 | e 01 355 270 2000 0.5000 2.00 00 0013 384 00 0 0 28 | 29 | 30 | g 02 03 88E2BE00 88E2BE63 88E2BE00 31 | a 02 0 0 1 summoning_animation_02.png 32 | c 02 0020 1 1.000 1.000 0.0 360.0 0 0 0.5000 1.0000 40.00 50.00 0.5000 0.5000 0 0 0 0 0.0 0.0 33 | e 02 355 270 2000 0.5000 2.00 01 0013 384 00 0 0 34 | 35 | 36 | g 03 02 5BD489FF 00000000 37 | a 03 0 0 1 summoning_animation_03.png 38 | c 03 0020 1 1.000 1.000 0.0 360.0 0 0 0.5000 1.0000 40.00 50.00 0.1000 0.0000 458 0 0 206 180.0 0.0 39 | e 03 355 270 2000 0.5000 2.00 00 0000 384 00 0 0 40 | 41 | 42 | g 04 03 08822BE6 E2D88800 6EC66300 43 | a 04 0 0 1 summoning_animation_04.png 44 | c 04 0001 1 1.000 1.000 0.0 360.0 0 0 0.5000 1.0000 0.00 0.00 0.0000 0.4206 0 0 0 0 172.9 186.9 45 | e 04 355 270 2000 0.5000 2.00 03 0005 256 00 0 0 46 | -------------------------------------------------------------------------------- /global.h: -------------------------------------------------------------------------------- 1 | /* ========================================================================= 2 | A particle editor made with raylib (https://github.com/raysan5/raylib) 3 | and raygui (https://github.com/raysan5/raygui) to be used with my very 4 | own particle library. 5 | WARNING: This is a very early alpha version so take care when using. 6 | 7 | TIP: use CTR+C/CTR+V outside the gui area to copy/paste emitters 8 | 9 | Inspired by: 10 | * pixijs editor (https://github.com/pixijs/pixi-particles-editor) 11 | * libpartikel (https://github.com/dbriemann/libpartikel) 12 | ========================================================================= 13 | LICENSE: zlib 14 | Copyright (C) 2021 Vlad Adrian (@Demizdor - https://github.com/Demizdor) 15 | ========================================================================= 16 | */ 17 | 18 | #pragma once 19 | 20 | #include "particles.h" 21 | 22 | #define EDITOR_VER "1.04 - ALPHA" 23 | 24 | #define MAX_EMITTERS 8 25 | #define MAX_PARTICLES 2000 26 | #define MAX_COLORS 16 27 | #define MAX_FORCES 4 28 | #define MAX_NAME_LEN 32 29 | #ifndef SIZEOF 30 | #define SIZEOF(A) (sizeof(A)/sizeof(A[0])) 31 | #endif 32 | 33 | 34 | struct SEditor 35 | { 36 | Emitter* emitters[MAX_EMITTERS]; 37 | int emitter_id[MAX_EMITTERS]; 38 | int emitter_count; 39 | 40 | int active_emitter; 41 | int active_color; 42 | int active_window; 43 | 44 | Camera2D camera; 45 | Vector2 offset, mouse; // used when moving 46 | 47 | Texture placeholder; 48 | 49 | Emitter* clipboard; 50 | 51 | struct SOptions 52 | { 53 | bool show_placeholder; 54 | bool show_grid; 55 | bool show_debug; 56 | Color gridcolor; 57 | Color debug; 58 | Color fg; 59 | Color bg; 60 | } options; 61 | 62 | struct SStatistics { 63 | bool shown; 64 | int drawn; // total number of particles drawn on the screen each frame 65 | int updated; // total number of particles updated per frame 66 | unsigned long long pixels; 67 | int total_mem; 68 | } statistics; 69 | 70 | char name[MAX_NAME_LEN]; 71 | } Editor; 72 | 73 | // --------------------------------------------------------------------------------------- 74 | // Global functions 75 | // --------------------------------------------------------------------------------------- 76 | Color GenerateRandomColor(float s, float v); 77 | void SetDefaultOptions(); 78 | void DrawGUI(); 79 | void DrawGridSystem(); 80 | bool CanMoveEmitter(); 81 | void DeallocateEmitters(); 82 | int SaveEmitters(const char* file); 83 | int LoadEmitters(const char* file); 84 | void EditorAddEmitter(Vector2 loc); 85 | void EditorRemoveEmitter(void); 86 | void EditorMoveUpEmitter(void); 87 | void EditorMoveDownEmitter(void); 88 | void EditorSyncEmitters(void); 89 | // --------------------------------------------------------------------------------------- 90 | -------------------------------------------------------------------------------- /gui.c: -------------------------------------------------------------------------------- 1 | /* ========================================================================= 2 | A particle editor made with raylib (https://github.com/raysan5/raylib) 3 | and raygui (https://github.com/raysan5/raygui) to be used with my very 4 | own particle library. 5 | WARNING: This is a very early alpha version so take care when using. 6 | 7 | TIP: use CTR+C/CTR+V outside the gui area to copy/paste emitters 8 | 9 | Inspired by: 10 | * pixijs editor (https://github.com/pixijs/pixi-particles-editor) 11 | * libpartikel (https://github.com/dbriemann/libpartikel) 12 | ========================================================================= 13 | LICENSE: zlib 14 | Copyright (C) 2021 Vlad Adrian (@Demizdor - https://github.com/Demizdor) 15 | ========================================================================= 16 | */ 17 | 18 | #include "global.h" 19 | 20 | #define RAYGUI_SUPPORT_ICONS 21 | #define RAYGUI_IMPLEMENTATION 22 | #include 23 | 24 | #define GUI_PROPERTY_LIST_IMPLEMENTATION 25 | #include "dm_property_list.h" 26 | 27 | #define GUI_BUTTON_SIZE 30 28 | #define GUI_WINDOW_SIZE 250 29 | 30 | // --------------------------------------------------------------------------------------- 31 | // GUI global variables 32 | // --------------------------------------------------------------------------------------- 33 | #define EMITTER_TYPE_NAMES "Point;Rect;Circle;Ring" 34 | #define EMITTER_BLEND_MODES "Alpha;Additive;Multiply;AddColors; SubColors" 35 | 36 | #define EASING_NAMES "Linear;SineIn;SineOut;SineInOut;CircIn;CircOut;CircInOut;CubicIn;CubicOut;CubicInOut; \ 37 | QuadIn;QuadOut;QuadInOut;ExpoIn;ExpoOut;ExpoInOut;BackIn;BackOut;BackInOut;BounceIn;BounceOut;BounceInOut; \ 38 | ElasticIn;ElasticOut;ElasticInOut" 39 | 40 | const Easing Easings[] = { 41 | &EaseLinearNone, &EaseSineIn, &EaseSineOut, &EaseSineInOut, 42 | &EaseCircIn, &EaseCircOut, &EaseCircInOut, 43 | &EaseCubicIn, &EaseCubicOut, &EaseCubicInOut, 44 | &EaseQuadIn, &EaseQuadOut, &EaseQuadInOut, 45 | &EaseExpoIn, &EaseExpoOut, &EaseExpoInOut, 46 | &EaseBackIn, &EaseBackOut, &EaseBackInOut, 47 | &EaseBounceIn, &EaseBounceOut, &EaseBounceInOut, 48 | &EaseElasticIn, &EaseElasticOut, &EaseElasticInOut 49 | }; 50 | // --------------------------------------------------------------------------------------- 51 | 52 | 53 | bool CanMoveEmitter() { 54 | return CheckCollisionPointRec(GetMousePosition(), (Rectangle){0.0f, 0.0f, GetScreenWidth()-GUI_WINDOW_SIZE-GUI_BUTTON_SIZE, GetScreenHeight()}); 55 | } 56 | 57 | void DrawGridSystem() 58 | { 59 | int color = GuiGetStyle(DEFAULT, LINE_COLOR); // get the default line color 60 | GuiSetStyle(DEFAULT, LINE_COLOR, ColorToInt(Editor.options.gridcolor)); // set our own color 61 | // make the grid tile infinitely based on the camera offset 62 | Rectangle grid = {fmodf(Editor.camera.offset.x, 48.0f)-96.0f, fmodf(Editor.camera.offset.y, 48.0f)-96.0f, GetScreenWidth()+144.0f, GetScreenHeight()+144.0f}; 63 | GuiGrid(grid, 48.0f, 4); 64 | GuiSetStyle(DEFAULT, LINE_COLOR, color); // reset line color to default 65 | } 66 | 67 | static void EmitterWindow(Rectangle bounds) 68 | { 69 | // emitter property enum in sync with the property array below it 70 | enum { 71 | EMISSION_EMITTER_PROP = 0, 72 | PULSES_EMITTER_PROP, 73 | 74 | FLAGS_SECTION_EMITTER_PROP, 75 | FLAGS_DISABLED_EMITTER_PROP, 76 | FLAGS_PAUSED_EMITTER_PROP, 77 | FLAGS_SPAWN_INSIDE_EMITTER_PROP, 78 | FLAGS_REVERSE_DRAW_EMITTER_PROP, 79 | FLAGS_LOCAL_SPACE_EMITTER_PROP, 80 | FLAGS_DRAW_TRIANGLES_EMITTER_PROP, 81 | FLAGS_DRAW_OUTLINE_EMITTER_PROP, 82 | FLAGS_DIRECTIONAL_ROTATION_EMITTER_PROP, 83 | FLAGS_LOOP_EMITTER_PROP, 84 | FLAGS_MULTITEXTURE_EMITTER_PROP, 85 | 86 | LIFE_EMITTER_PROP, 87 | EASING_EMITTER_PROP, 88 | DELAY_EMITTER_PROP, 89 | 90 | BLEND_MODES_EMITTER_PROP, 91 | 92 | SIZE_SECTION_EMITTER_PROP, 93 | SIZE_MIN_EMITTER_PROP, 94 | SIZE_MAX_EMITTER_PROP, 95 | 96 | SCALE_SECTION_EMITTER_PROP, 97 | SCALE_START_EMITTER_PROP, 98 | SCALE_END_EMITTER_PROP, 99 | 100 | ROTATION_SECTION_EMITTER_PROP, 101 | ROTATION_START_EMITTER_PROP, 102 | ROTATION_END_EMITTER_PROP, 103 | 104 | ANGLE_SECTION_EMITTER_PROP, 105 | ANGLE_MIN_EMITTER_PROP, 106 | ANGLE_MAX_EMITTER_PROP, 107 | 108 | SPEED_SECTION_EMITTER_PROP, 109 | SPEED_MIN_EMITTER_PROP, 110 | SPEED_MAX_EMITTER_PROP, 111 | 112 | AGE_SECTION_EMITTER_PROP, 113 | AGE_MIN_EMITTER_PROP, 114 | AGE_MAX_EMITTER_PROP, 115 | 116 | OFFSET_SECTION_EMITTER_PROP, 117 | OFFSET_MIN_EMITTER_PROP, 118 | OFFSET_MAX_EMITTER_PROP, 119 | 120 | ACC_SECTION_EMITTER_PROP, 121 | ACC_START_EMITTER_PROP, 122 | ACC_END_EMITTER_PROP, 123 | 124 | TACC_SECTION_EMITTER_PROP, 125 | TACC_START_EMITTER_PROP, 126 | TACC_END_EMITTER_PROP, 127 | 128 | TYPE_EMITTER_PROP, 129 | // the last 2 properties are not needed 130 | }; 131 | 132 | // Global emitter property array 133 | static GuiDMProperty prop[] = 134 | { 135 | PINTPTR_RANGE("Emission", 0, NULL, 1, 1, MAX_PARTICLES-1), 136 | PINTPTR_RANGE("Pulses", 0, NULL, 1, 0, 128), 137 | 138 | PSECTION("Flags", 0, 9), 139 | PBOOLPTR("disabled", 0, NULL), 140 | PBOOLPTR("paused", 0, NULL), 141 | PBOOLPTR("spawn inside", 0, NULL), 142 | PBOOLPTR("reverse draw", 0, NULL), 143 | PBOOLPTR("local space", 0, NULL), 144 | PBOOLPTR("triangles", 0, NULL), 145 | PBOOLPTR("outline", 0, NULL), 146 | PBOOLPTR("directional rot", 0, NULL), 147 | PBOOLPTR("loop", 0, NULL), 148 | PBOOLPTR("mutitextures", 0, NULL), 149 | 150 | PFLOATPTR_RANGE("Life", 0, NULL, 1.0, 2, 0.0f, 600.0f), 151 | PSELECT("Easing", 0, (char*)EASING_NAMES, 0), 152 | 153 | PFLOATPTR_RANGE("Delay", 0, NULL, 1.0, 3, 0.0f, 60.0f), 154 | 155 | PSELECT("Blend Mode", 0, (char*)EMITTER_BLEND_MODES, 0), 156 | 157 | PSECTION("Size", 0, 2), 158 | PFLOATPTR_RANGE("min", 0, NULL, 0.1f, 2, 0.0f, 10.0f), 159 | PFLOATPTR_RANGE("max", 0, NULL, 0.1f, 2, 0.0f, 10.0f), 160 | 161 | PSECTION("Scale", 0, 2), 162 | PFLOATPTR_RANGE("start", 0, NULL, 0.1f, 2, 0.0f, 10.0f), 163 | PFLOATPTR_RANGE("end", 0, NULL, 0.1f, 2, 0.0f, 10.0f), 164 | 165 | PSECTION("Rotation", 0, 2), 166 | PFLOATPTR_RANGE("start", 0, NULL, 0.1f, 2, 0.0f, 1000.0f), 167 | PFLOATPTR_RANGE("end", 0, NULL, 0.1f, 2, 0.0f, 1000.0f), 168 | 169 | PSECTION("Angle", 0, 2), 170 | PFLOATPTR_RANGE("min", 0, NULL, 0.1f, 2, -360.0f, 360.0f), 171 | PFLOATPTR_RANGE("max", 0, NULL, 0.1f, 2, -360.0f, 360.0f), 172 | 173 | PSECTION("Speed", 0, 2), 174 | PFLOATPTR_RANGE("min", 0, NULL, 1.0f, 2, 0.0f, 2000.0f), 175 | PFLOATPTR_RANGE("max", 0, NULL, 1.0f, 2, 0.0f, 2000.0f), 176 | 177 | PSECTION("Age", 0, 2), 178 | PFLOATPTR_RANGE("min", 0, NULL, 1.0f, 2, 0.0f, 100.0f), 179 | PFLOATPTR_RANGE("max", 0, NULL, 1.0f, 2, 0.0f, 100.0f), 180 | 181 | PSECTION("Offset", 0, 2), 182 | PFLOATPTR_RANGE("min", 0, NULL, 1.0f, 0, -1000.0f, 1000.0f), 183 | PFLOATPTR_RANGE("max", 0, NULL, 1.0f, 0, -1000.0f, 1000.0f), 184 | 185 | PSECTION("Acc", 0, 2), 186 | PFLOATPTR_RANGE("start", 0, NULL, 1.0f, 2, -1000.0f, 1000.0f), 187 | PFLOATPTR_RANGE("end", 0, NULL, 1.0f, 2, -1000.0f, 1000.0f), 188 | 189 | PSECTION("TAcc", 0, 2), 190 | PFLOATPTR_RANGE("start", 0, NULL, 1.0f, 2, -1000.0f, 1000.0f), 191 | PFLOATPTR_RANGE("end", 0, NULL, 1.0f, 2, -1000.0f, 1000.0f), 192 | 193 | PSELECT("Type", 0, (char*)EMITTER_TYPE_NAMES, 0), 194 | PINT("[1]", GUI_PFLAG_DISABLED, 0), 195 | PINT("[2]", GUI_PFLAG_DISABLED, 0), 196 | }; 197 | 198 | Emitter* e = Editor.emitters[Editor.active_emitter]; // get the active emitter 199 | 200 | // get index of the easing used by the active emitter 201 | // using a map will be better but meh, too much work 202 | int easing_idx = 0; 203 | for(int i=0; iconfig.easing) { easing_idx = i; break; } 205 | } 206 | 207 | // populate a array will all the flags used by the active emitter 208 | bool flags[] = { 209 | FLAG_CHECK(e->flags, EMITTER_FLAG_DISABLED), 210 | FLAG_CHECK(e->flags, EMITTER_FLAG_PAUSED), 211 | FLAG_CHECK(e->flags, EMITTER_FLAG_SPAWN_INSIDE), 212 | FLAG_CHECK(e->flags, EMITTER_FLAG_REVERSE_DRAW_ORDER), 213 | FLAG_CHECK(e->flags, EMITTER_FLAG_WORLD_SPACE), 214 | FLAG_CHECK(e->flags, EMITTER_FLAG_DRAW_TRIANGLES), 215 | FLAG_CHECK(e->flags, EMITTER_FLAG_DRAW_OUTLINE), 216 | FLAG_CHECK(e->flags, EMITTER_FLAG_DIRECTIONAL_ROTATION), 217 | FLAG_CHECK(e->flags, EMITTER_FLAG_LOOP), 218 | FLAG_CHECK(e->flags, EMITTER_FLAG_MULTITEXTURE), 219 | }; 220 | 221 | // get initial pulses for this emitter (if this changes we need to reset the particles) 222 | int pulses = e->config.pulses; 223 | 224 | // overwrite the property array values with the actual values for this emitter 225 | PSET_INTPTR(prop, EMISSION_EMITTER_PROP, &e->config.emission); 226 | PSET_INTPTR(prop, PULSES_EMITTER_PROP, &e->config.pulses); 227 | // set all the flags 228 | for(int i=0; ilife); 232 | PSET_SELECT_ACTIVE(prop, EASING_EMITTER_PROP, easing_idx); 233 | PSET_FLOATPTR(prop, DELAY_EMITTER_PROP, &e->delay); 234 | PSET_SELECT_ACTIVE(prop, BLEND_MODES_EMITTER_PROP, e->mode); 235 | PSET_FLOATPTR(prop, SIZE_MIN_EMITTER_PROP, &e->config.size.min); 236 | PSET_FLOATPTR(prop, SIZE_MAX_EMITTER_PROP, &e->config.size.max); 237 | PSET_FLOATPTR(prop, SCALE_START_EMITTER_PROP, &e->config.scale.start); 238 | PSET_FLOATPTR(prop, SCALE_END_EMITTER_PROP, &e->config.scale.end); 239 | PSET_FLOATPTR(prop, ROTATION_START_EMITTER_PROP, &e->config.rotation.start); 240 | PSET_FLOATPTR(prop, ROTATION_END_EMITTER_PROP, &e->config.rotation.end); 241 | PSET_FLOATPTR(prop, ANGLE_MIN_EMITTER_PROP, &e->config.angle.min); 242 | PSET_FLOATPTR(prop, ANGLE_MAX_EMITTER_PROP, &e->config.angle.max); 243 | PSET_FLOATPTR(prop, SPEED_MIN_EMITTER_PROP, &e->config.speed.min); 244 | PSET_FLOATPTR(prop, SPEED_MAX_EMITTER_PROP, &e->config.speed.max); 245 | PSET_FLOATPTR(prop, AGE_MIN_EMITTER_PROP, &e->config.age.min); 246 | PSET_FLOATPTR(prop, AGE_MAX_EMITTER_PROP, &e->config.age.max); 247 | PSET_FLOATPTR(prop, OFFSET_MIN_EMITTER_PROP, &e->config.offset.min); 248 | PSET_FLOATPTR(prop, OFFSET_MAX_EMITTER_PROP, &e->config.offset.max); 249 | PSET_FLOATPTR(prop, ACC_START_EMITTER_PROP, &e->config.acc.start); 250 | PSET_FLOATPTR(prop, ACC_END_EMITTER_PROP, &e->config.acc.end); 251 | PSET_FLOATPTR(prop, TACC_START_EMITTER_PROP, &e->config.tacc.start); 252 | PSET_FLOATPTR(prop, TACC_END_EMITTER_PROP, &e->config.tacc.end); 253 | PSET_SELECT_ACTIVE(prop, TYPE_EMITTER_PROP, e->config.container.type); 254 | 255 | 256 | // change the container properties based on the emitter type 257 | int type = e->config.container.type; 258 | if(type == EMITTER_RECT) 259 | { 260 | prop[SIZEOF(prop)-2] = PFLOATPTR_RANGE("Width", 0, &e->config.container.opt1, 1.0f, 0, 0.0f, 4096.0f); 261 | prop[SIZEOF(prop)-1] = PFLOATPTR_RANGE("Height", 0, &e->config.container.opt2, 1.0f, 0, 0.0f, 4096.0f); 262 | } 263 | else if(type == EMITTER_CIRCLE) 264 | { 265 | prop[SIZEOF(prop)-2] = PFLOATPTR_RANGE("Radius", 0, &e->config.container.opt1, 1.0f, 0, 0.0f, 4096.0f); 266 | prop[SIZEOF(prop)-1] = PINT("[2]", GUI_PFLAG_DISABLED, 0); 267 | } 268 | else if(type == EMITTER_RING) 269 | { 270 | prop[SIZEOF(prop)-2] = PFLOATPTR_RANGE("Min Radius", 0, &e->config.container.opt1, 1.0f, 0, 0.0f, 4096.0f); 271 | prop[SIZEOF(prop)-1] = PFLOATPTR_RANGE("Max Radius", 0, &e->config.container.opt2, 1.0f, 0, 0.0f, 4096.0f); 272 | } 273 | else 274 | { 275 | prop[SIZEOF(prop)-2] = PINT("[1]", GUI_PFLAG_DISABLED, 0); 276 | prop[SIZEOF(prop)-1] = PINT("[2]", GUI_PFLAG_DISABLED, 0); 277 | } 278 | 279 | 280 | // draw the properties 281 | static int focused = 0; 282 | static int scroll = 0; 283 | GuiDMPropertyList(bounds, prop, SIZEOF(prop), &focused, &scroll); 284 | 285 | // get changed properties 286 | easing_idx = PGET_SELECT_ACTIVE(prop, EASING_EMITTER_PROP); 287 | e->config.easing = Easings[easing_idx]; 288 | e->mode = PGET_SELECT_ACTIVE(prop, BLEND_MODES_EMITTER_PROP); 289 | e->config.container.type = PGET_SELECT_ACTIVE(prop, TYPE_EMITTER_PROP); 290 | // set flags 291 | for(int i=0; iflags, 1<flags, 1<config.pulses) { 297 | memset(e->particles.data, 0, MAX_PARTICLES*sizeof(Particle)); 298 | e->particles.count = 0; 299 | } 300 | } 301 | 302 | static void ColorWindow(Rectangle bounds) 303 | { 304 | GuiPanel(bounds); 305 | 306 | Emitter* e = Editor.emitters[Editor.active_emitter]; // get the active emitter 307 | 308 | if(Editor.active_color >= e->config.gradient.count) Editor.active_color = 0; 309 | 310 | // setup color changer 311 | GuiDMProperty prop[] = { 312 | PCOLOR("Color", 0, 0, 0, 0, 0), 313 | }; 314 | PSET_COLOR(prop, 0, e->config.gradient.colors[Editor.active_color]); 315 | 316 | // draw color changer 317 | Rectangle item = (Rectangle){bounds.x+2, bounds.y+2, bounds.width-4, 180}; 318 | static int focus = 0; 319 | static int scroll = 0; 320 | GuiDMPropertyList(item, prop, SIZEOF(prop), &focus, &scroll); 321 | e->config.gradient.colors[Editor.active_color] = PGET_COLOR(prop, 0); 322 | 323 | GuiSetStyle(LABEL, TEXT_ALIGNMENT, GUI_TEXT_ALIGN_CENTER); 324 | 325 | // draw available colors 326 | Rectangle cr = (Rectangle){item.x, item.y+200, 30, 30}; 327 | for(int i=0; i < e->config.gradient.count; ++i) { 328 | if(GuiLabelButton(cr, TextFormat("# %i", i))) Editor.active_color = i; // set this color as the active color on click 329 | GuiDrawRectangle(cr, 2, Editor.active_color != i ? BLACK : RED, e->config.gradient.colors[i]); 330 | if(cr.x + 2*35 >= item.x + GUI_WINDOW_SIZE) { 331 | cr.x = item.x; cr.y += 35; 332 | } else cr.x += 35; 333 | } 334 | 335 | // draw the Add/Remove color buttons 336 | cr.width = GUI_WINDOW_SIZE/2 - 4; 337 | if(cr.x != item.x) cr.y += 35; 338 | cr.x = item.x; 339 | if(GuiButton(cr, "Add")) { 340 | if(e->config.gradient.count < MAX_COLORS) { 341 | e->config.gradient.colors[e->config.gradient.count] = GenerateRandomColor(0.5f, 0.78f); 342 | Editor.active_color = e->config.gradient.count; 343 | e->config.gradient.count += 1; 344 | } 345 | } 346 | cr.x += cr.width + 2; 347 | if(GuiButton(cr, "Remove")) { 348 | if(e->config.gradient.count > 1) e->config.gradient.count -= 1; 349 | if(Editor.active_color >= e->config.gradient.count) Editor.active_color = e->config.gradient.count - 1; 350 | } 351 | 352 | // draw color gradient 353 | cr.y += 35; 354 | int cw = (GUI_WINDOW_SIZE - 4)/e->config.gradient.count; 355 | for(int i=0; i < e->config.gradient.count; ++i) { 356 | Rectangle cb = {item.x+cw*i, cr.y, cw, GetScreenHeight()-45-cr.y-3}; 357 | DrawRectangleRec(cb, e->config.gradient.colors[i]); 358 | } 359 | 360 | GuiSetStyle(LABEL, TEXT_ALIGNMENT, GUI_TEXT_ALIGN_LEFT); 361 | } 362 | 363 | static void ForcesWindow(Rectangle bounds) 364 | { 365 | GuiPanel(bounds); 366 | Emitter* e = Editor.emitters[Editor.active_emitter]; // get the active emitter 367 | 368 | Rectangle item = {bounds.x+2, bounds.y+2, bounds.width-4, 0}; 369 | // populate the forces property list 370 | if(e->config.forces.count != 0) 371 | { 372 | item.height = e->config.forces.count*98; 373 | int max_height = bounds.height - 82; 374 | if(item.height > max_height) item.height = max_height; 375 | GuiDMProperty prop[MAX_FORCES*3]; 376 | 377 | for(int i=0, p=0; i< e->config.forces.count; ++i, p+=3) { 378 | prop[p] = PSECTION((char* )TextFormat("Force %i", i+1), 0, 2); 379 | prop[p+1] = PFLOATPTR_RANGE("direction", 0, &e->config.forces.data[i].direction, 1.0f, 2, -360.0f, 360.0f); 380 | prop[p+2] = PFLOATPTR_RANGE("strength", 0, &e->config.forces.data[i].strength, 1.0f, 2, -1000.0f, 1000.0f); 381 | } 382 | 383 | // draw forces list 384 | static int focus = 0; 385 | static int scroll = 0; 386 | GuiDMPropertyList(item, prop, e->config.forces.count*3, &focus, &scroll); 387 | 388 | } 389 | 390 | // draw the Add/Remove buttons 391 | item.y += item.height + 10; 392 | item.height = 30; 393 | if(GuiButton(item, "Add Force")) { 394 | if(e->config.forces.count < MAX_FORCES) e->config.forces.count += 1; 395 | } 396 | item.y += 35; 397 | if(GuiButton(item, "Remove Force")) { 398 | if(e->config.forces.count > 0) e->config.forces.count -= 1; 399 | } 400 | } 401 | 402 | static void OptionsWindow(Rectangle bounds) 403 | { 404 | GuiPanel(bounds); 405 | Rectangle item = (Rectangle){bounds.x+2, bounds.y+2, bounds.width-2, GetScreenHeight()-99}; 406 | static GuiDMProperty prop[] = { 407 | PBOOLPTR("Grid", 0, &Editor.options.show_grid), 408 | PBOOLPTR("Placeholder", 0, &Editor.options.show_placeholder), 409 | PBOOLPTR("Debug", 0, &Editor.options.show_debug), 410 | 411 | PCOLOR("Background", 0, 0, 0, 0, 0), 412 | PCOLOR("Foreground", 0, 0, 0, 0, 0), 413 | PCOLOR("Grid Color", 0, 0, 0, 0, 0), 414 | PCOLOR("Debug", 0, 0, 0, 0, 0) 415 | }; 416 | 417 | PSET_COLOR(prop, 3, Editor.options.bg); 418 | PSET_COLOR(prop, 4, Editor.options.fg); 419 | PSET_COLOR(prop, 5, Editor.options.gridcolor); 420 | PSET_COLOR(prop, 6, Editor.options.debug); 421 | 422 | static int focus = 0; 423 | static int scroll = 0; 424 | GuiDMPropertyList(item, prop, SIZEOF(prop), &focus, &scroll); 425 | 426 | Editor.options.bg = PGET_COLOR(prop, 3); 427 | Editor.options.fg = PGET_COLOR(prop, 4); 428 | Editor.options.gridcolor = PGET_COLOR(prop, 5); 429 | Editor.options.debug = PGET_COLOR(prop, 6); 430 | 431 | item.y += item.height + 5; 432 | item.x += 10; 433 | item.width -= 20; 434 | item.height = 40; 435 | if(GuiButton(item, "Reset")) SetDefaultOptions(); 436 | } 437 | 438 | static void TextureWindow(Rectangle bounds) 439 | { 440 | GuiPanel(bounds); 441 | Emitter* e = Editor.emitters[Editor.active_emitter]; // get the active emitter 442 | 443 | Rectangle item = {bounds.x+2, bounds.y+2, bounds.width-4, bounds.width-4}; 444 | if(e->config.atlas.texture.id < 1) 445 | { 446 | GuiDrawText("NO TEXTURE", item, GUI_TEXT_ALIGN_CENTER, GRAY); 447 | GuiDrawRectangle(item, 2, GRAY, BLANK); 448 | } 449 | else 450 | { 451 | Rectangle src = {0.0f, 0.0f, e->config.atlas.texture.width, e->config.atlas.texture.height}; 452 | GuiDrawRectangle(item, 0, BLANK, BLACK); 453 | DrawTexturePro(e->config.atlas.texture, src, item, (Vector2){0,0}, 0.0f, WHITE); 454 | 455 | item.y += bounds.width+6; 456 | item.height = 30; 457 | if(GuiButton(item, "Clear Texture")) 458 | { 459 | bool unload = true; 460 | // only unload if no other emitter has the texture in use 461 | for(int i=0; iconfig.atlas.texture.id == e->config.atlas.texture.id) { 463 | unload = false; 464 | break; 465 | } 466 | } 467 | 468 | // check for clipboard texture 469 | if(Editor.clipboard != NULL && e->config.atlas.texture.id == Editor.clipboard->config.atlas.texture.id) { 470 | unload = false; 471 | } 472 | 473 | if(unload) UnloadTexture(e->config.atlas.texture); 474 | 475 | e->config.atlas.texture = (Texture){0}; 476 | e->config.atlas.vframes = e->config.atlas.hframes = 0; 477 | } 478 | 479 | // draw texture properties 480 | item.y += 40; 481 | item.height = 160; 482 | GuiDMProperty prop[] = { 483 | PSECTION("Frames", 0, 2), 484 | PINTPTR_RANGE("horizontal", 0, &e->config.atlas.hframes, 1, 0, 256), 485 | PINTPTR_RANGE("vertical", 0, &e->config.atlas.vframes, 1, 0, 256), 486 | PINTPTR_RANGE("loop", 0, &e->config.atlas.loop, 1, 1, 256), 487 | }; 488 | 489 | static int focus = 0; 490 | static int scroll = 0; 491 | GuiDMPropertyList(item, prop, SIZEOF(prop), &focus, &scroll); 492 | } 493 | } 494 | 495 | // Draw the right side window 496 | static void DrawWindow(Rectangle bounds) 497 | { 498 | // draw the curently active window 499 | static void (*window[])(Rectangle bounds) = { &EmitterWindow, &ColorWindow, &TextureWindow, &ForcesWindow, &OptionsWindow}; 500 | (*window[Editor.active_window])(bounds); 501 | 502 | // draw the tabs below it 503 | Editor.active_window = GuiToggleGroup((Rectangle){bounds.x, GetScreenHeight()-40, 30, 30}, "#96#;#27#;#12#;#147#;#140#", Editor.active_window); 504 | int pressed = GuiToggleGroup((Rectangle){bounds.x+bounds.width-60, GetScreenHeight()-40, 58, 30}, "#6#Save", -1); 505 | if(pressed == 0) { 506 | // Save particle system to file 507 | static int i = 1; 508 | const char* file = TextFormat("%s_%00i.dps", Editor.name, i); 509 | if(SaveEmitters(file)) 510 | { 511 | TraceLog(LOG_INFO , TextFormat("Saved emitters as `%s`", file)); 512 | ++i; 513 | } else { 514 | TraceLog(LOG_WARNING, TextFormat("Failed to save emitters as `%s`", file)); 515 | } 516 | } 517 | } 518 | 519 | // Draw the editor buttons at the right of the screen 520 | void EditorButtons(Rectangle bounds) 521 | { 522 | GuiSetStyle(LABEL, TEXT_ALIGNMENT, GUI_TEXT_ALIGN_CENTER); 523 | 524 | // Draw all the available emitters 525 | for(int i=0; i S*S) { OUT = IN/(S*S); U = "M"; } \ 586 | else if(IN > S) { OUT = IN/S; U = "K"; } \ 587 | else { OUT = IN; U = ""; } \ 588 | } while(0) 589 | 590 | void DrawGUI() 591 | { 592 | // Calculate bounds for the window on the right side 593 | Rectangle bounds = {GetScreenWidth()-GUI_WINDOW_SIZE-GUI_BUTTON_SIZE, 2, GUI_BUTTON_SIZE, GUI_BUTTON_SIZE}; 594 | if(Editor.active_emitter == -1) bounds.x += GUI_WINDOW_SIZE; 595 | 596 | // Draw FPS 597 | const char* fmt = Editor.statistics.shown ? "#120#FPS %i" : "#119#FPS %i"; 598 | if(GuiLabelButton((Rectangle){10.0f, 8.0f, 50.0f, 20.0f}, TextFormat(fmt, GetFPS())) ) Editor.statistics.shown = !Editor.statistics.shown; 599 | 600 | // Draw Statistics 601 | if(Editor.statistics.shown) 602 | { 603 | double pixels = 0.0; 604 | char* pu = ""; 605 | FORMAT_MEASUREMENT(Editor.statistics.pixels, pixels, pu, 1000.0); 606 | 607 | int used_mem = 0; 608 | char* umu = ""; 609 | for(int i=0; iparticles.count*sizeof(Particle) + 611 | Editor.emitters[i]->config.gradient.count*sizeof(Color) + Editor.emitters[i]->config.forces.count*sizeof(Force); 612 | } 613 | FORMAT_MEASUREMENT(used_mem, used_mem, umu, 1024); 614 | 615 | int total_mem = Editor.statistics.total_mem; 616 | char* tmu = ""; 617 | FORMAT_MEASUREMENT(total_mem, total_mem, tmu, 1024); 618 | 619 | DrawText(TextFormat("updated %d\ndrawn %d\npixels %.2f%s\nused_mem %d%s\ntotal_mem %d%s", Editor.statistics.updated, Editor.statistics.drawn, 620 | pixels, pu, used_mem, umu, total_mem, tmu), 10, 35, 10, Editor.options.fg); 621 | } 622 | 623 | // Draw the name input at the top of the screen 624 | EditorNameInput((Rectangle){74.0f, 2.0f, (Editor.active_emitter == -1) ? GetScreenWidth()-GUI_BUTTON_SIZE-70 : GetScreenWidth()-GUI_WINDOW_SIZE-GUI_BUTTON_SIZE-70, GUI_BUTTON_SIZE}); 625 | 626 | // Draw the buttons on the right side 627 | EditorButtons(bounds); 628 | 629 | if(Editor.active_emitter == -1 || Editor.emitter_count == 0 ) return; 630 | 631 | // Draw the window on the right side 632 | bounds = (Rectangle){GetScreenWidth()-GUI_WINDOW_SIZE, -1, GUI_WINDOW_SIZE+1, GetScreenHeight()-45 }; 633 | DrawWindow(bounds); 634 | 635 | // Show the tooltips (we do it here so that the tootips will show on top of everything) 636 | Vector2 mouse = GetMousePosition(); 637 | GuiDrawTooltip((Rectangle){mouse.x-10.0f, mouse.y-10.0f, 100.0f, 30.0f}); 638 | GuiClearTooltip(); 639 | } 640 | 641 | static inline int WasTextureExported(int tidx, int id) 642 | { 643 | for(int i=0; iconfig.atlas.texture.id == tidx) return i; 645 | } 646 | return id; 647 | } 648 | 649 | int SaveEmitters(const char* file) 650 | { 651 | if(Editor.emitter_count == 0) return 0; 652 | 653 | FILE* fp = fopen(file, "wb"); 654 | if(fp == NULL) return 0; 655 | 656 | //write help 657 | fprintf(fp, "#\n" 658 | "# Demizdor's Particle System file (v" EDITOR_VER ")\n" 659 | "#\n" 660 | "# Emitter properties:\n" 661 | "#\tn name_of_particle_system\n" 662 | "#\tp placeholder_texture\n" 663 | "#\n" 664 | "#\tg ID gradient_count g0 g1 ... gn\n" 665 | "#\tf ID forces_count f0_direction f0_strength .. fn_direction fn_strength\n" 666 | "#\ta ID hframes vframes loop texture\n" 667 | "#\tc ID emission pulses size_min size_max angle_min angle_max offset_min offset_max age_min age_max speed_min speed_max scale_start scale_end acc_start acc_end tacc_start tacc_end rot_start rot_end\n" 668 | "#\te ID pos_x pos_y max_particles life delay blend_mode easing flags type opt1 opt2\n" 669 | "#\n"); 670 | 671 | // write placeholder and name of the particle system 672 | fprintf(fp, "\nn %s\n", Editor.name); 673 | if(Editor.placeholder.id > 0) { 674 | ExportImage(GetTextureData(Editor.placeholder), TextFormat("%s/%s_ph.png", GetDirectoryPath(file), Editor.name)); 675 | fprintf(fp, "p %s_ph.png\n", Editor.name); 676 | } 677 | 678 | 679 | 680 | for(int i=0; iconfig.gradient.count); 686 | for(int g=0; gconfig.gradient.count; ++g) { 687 | fprintf(fp, " %08X", ColorToInt(e->config.gradient.colors[g])); 688 | } 689 | fprintf(fp, "\n"); 690 | 691 | // write forces 692 | if(e->config.forces.count > 0) { 693 | fprintf(fp, "f %02i %02i", i, e->config.forces.count); 694 | for(int f=0; fconfig.forces.count; ++f) { 695 | fprintf(fp, " %.2f %.2f", e->config.forces.data[f].direction, e->config.forces.data[f].strength); 696 | } 697 | fprintf(fp, "\n"); 698 | } 699 | 700 | // write atlas 701 | if(e->config.atlas.texture.id > 0) 702 | { 703 | int id = WasTextureExported(e->config.atlas.texture.id, i); 704 | if(id == i) ExportImage(GetTextureData(e->config.atlas.texture), TextFormat("%s/%s_%02i.png", GetDirectoryPath(file), Editor.name, i)); 705 | fprintf(fp, "a %02i %3i %3i %3i %s_%02i.png\n", i, e->config.atlas.hframes, e->config.atlas.vframes, e->config.atlas.loop, Editor.name, id); 706 | } 707 | 708 | // find the easing id 709 | int easing_idx = 0; 710 | for(int k=0; kconfig.easing) { 712 | easing_idx = k; 713 | break; 714 | } 715 | } 716 | // write configuration 717 | fprintf(fp, "c %02i %04i %2i %f %f %.1f %.1f %.0f %.0f %f %f %f %f %f %f %.0f %.0f %.0f %.0f %.1f %.1f\n", i, 718 | e->config.emission, e->config.pulses, e->config.size.min, e->config.size.max, e->config.angle.min, e->config.angle.max, 719 | e->config.offset.min, e->config.offset.max, e->config.age.min, e->config.age.max, e->config.speed.min, e->config.speed.max, e->config.scale.start, e->config.scale.end, 720 | e->config.acc.start, e->config.acc.end, e->config.tacc.start, e->config.tacc.end, e->config.rotation.start, e->config.rotation.end); 721 | 722 | // write emitter 723 | // e ID pos_x pos_y max_particles life delay blend_mode easing flags type opt1 opt2 724 | fprintf(fp, "e %02i %.0f %.0f %04i %f %f %02i %04i %i %02i %4.0f %4.0f\n", i, e->position.x, e->position.y, e->particles.max, e->life, e->delay, e->mode, easing_idx, e->flags, 725 | e->config.container.type, e->config.container.opt1, e->config.container.opt2); 726 | } 727 | 728 | fclose(fp); 729 | 730 | return 1; 731 | } 732 | 733 | int LoadEmitters(const char* file) 734 | { 735 | FILE* fp = fopen(file, "rb"); 736 | if(fp == NULL) return 0; 737 | 738 | enum {MAX_BUFFER_SIZE = 512}; 739 | 740 | char buffer[MAX_BUFFER_SIZE] = {0}; 741 | fgets(buffer, MAX_BUFFER_SIZE, fp); 742 | if(buffer[0] != '#') { fclose(fp); return 0; } 743 | 744 | // reset emitters 745 | DeallocateEmitters(); 746 | Editor.emitter_count = 0; 747 | Editor.active_emitter = -1; 748 | // reset emitter ids 749 | for(int i=0; iconfig.gradient.colors == NULL) { 770 | Editor.emitters[id]->config.gradient.colors = calloc(MAX_COLORS, sizeof(Color)); 771 | Editor.statistics.total_mem += MAX_COLORS*sizeof(Color); 772 | } 773 | 774 | if(Editor.emitters[id]->config.gradient.colors != NULL) 775 | { 776 | idx += bytes; 777 | for(int c=0; cconfig.gradient.colors[c] = GetColor(color); 785 | } 786 | Editor.emitters[id]->config.gradient.count = count; 787 | } else Editor.emitters[id]->config.gradient.count = 0; 788 | } 789 | } 790 | else if(buffer[0] == 'f') 791 | { 792 | // parse forces 793 | int id = MAX_EMITTERS, count = 0, idx = 0, bytes = 0;; 794 | sscanf(buffer, "f %02d %02d%n", &id, &count, &bytes); 795 | if(id < MAX_EMITTERS && count <= MAX_FORCES) 796 | { 797 | if(Editor.emitters[id] == NULL) { 798 | // allocate memory for emitter 799 | Editor.emitters[id] = calloc(1, sizeof(Emitter)); 800 | if(Editor.emitters[id] == NULL) TraceLog(LOG_FATAL, "EMITTER: Failed to allocate memory"); 801 | Editor.statistics.total_mem += sizeof(Emitter); 802 | } 803 | 804 | if(Editor.emitters[id]->config.forces.data == NULL) { 805 | Editor.emitters[id]->config.forces.data = calloc(MAX_FORCES, sizeof(Force)); 806 | Editor.statistics.total_mem += MAX_FORCES*sizeof(Force); 807 | } 808 | 809 | if(Editor.emitters[id]->config.forces.data != NULL) { 810 | idx += bytes; 811 | for(int f=0; fconfig.forces.data[f].direction, 813 | &Editor.emitters[id]->config.forces.data[f].strength, &bytes) == EOF) 814 | { 815 | count = f + 1; 816 | break; 817 | } 818 | idx += bytes; 819 | } 820 | Editor.emitters[id]->config.forces.count = count; 821 | } else Editor.emitters[id]->config.forces.count = 0; 822 | } 823 | } 824 | else if(buffer[0] == 'a') 825 | { 826 | // parse atlas 827 | int id = MAX_EMITTERS, idx=0, bytes=0; 828 | sscanf(buffer, "a %02d%n", &id, &bytes); 829 | if(id < MAX_EMITTERS) 830 | { 831 | if(Editor.emitters[id] == NULL) { 832 | // allocate memory for emitter 833 | Editor.emitters[id] = calloc(1, sizeof(Emitter)); 834 | if(Editor.emitters[id] == NULL) TraceLog(LOG_FATAL, "EMITTER: Failed to allocate memory"); 835 | Editor.statistics.total_mem += sizeof(Emitter); 836 | } 837 | 838 | idx += bytes; 839 | char texture[128] = {0}; 840 | Emitter* e = Editor.emitters[id]; 841 | sscanf(&buffer[idx], " %3d %3d %3d %127[^\r\n]s\n", &e->config.atlas.hframes, &e->config.atlas.vframes, &e->config.atlas.loop, (char*)&texture); 842 | int has_texture = strncmp("NONE", texture, 4); 843 | if(has_texture != 0) { 844 | Texture t = LoadTexture(TextFormat("%s/%s", GetDirectoryPath(file), texture)); 845 | if(t.id > 0) { 846 | if(Editor.clipboard == NULL || (Editor.clipboard != NULL && Editor.clipboard->config.atlas.texture.id != e->config.atlas.texture.id)) 847 | UnloadTexture(e->config.atlas.texture); 848 | e->config.atlas.texture = t; 849 | } 850 | } 851 | } 852 | } 853 | else if(buffer[0] == 'c') 854 | { 855 | // parse configuration 856 | int id = MAX_EMITTERS, idx=0, bytes=0; 857 | sscanf(buffer, "c %02d%n", &id, &bytes); 858 | if(id < MAX_EMITTERS) 859 | { 860 | if(Editor.emitters[id] == NULL) { 861 | // allocate memory for emitter 862 | Editor.emitters[id] = calloc(1, sizeof(Emitter)); 863 | if(Editor.emitters[id] == NULL) TraceLog(LOG_FATAL, "EMITTER: Failed to allocate memory"); 864 | Editor.statistics.total_mem += sizeof(Emitter); 865 | } 866 | 867 | idx += bytes; 868 | EmitterConfig* cfg = &Editor.emitters[id]->config; 869 | sscanf(&buffer[idx], " %04d %2d %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f", 870 | &cfg->emission, &cfg->pulses, &cfg->size.min, &cfg->size.max, &cfg->angle.min, &cfg->angle.max, &cfg->offset.min, &cfg->offset.max, 871 | &cfg->age.min, &cfg->age.max, &cfg->speed.min, &cfg->speed.max, &cfg->scale.start, &cfg->scale.end, &cfg->acc.start, &cfg->acc.end, 872 | &cfg->tacc.start, &cfg->tacc.end, &cfg->rotation.start, &cfg->rotation.end); 873 | } 874 | } 875 | else if(buffer[0] == 'e') 876 | { 877 | // parse emitter 878 | int id = MAX_EMITTERS, idx = 0, bytes = 0; 879 | sscanf(buffer, "e %02d%n", &id, &bytes); 880 | if(id < MAX_EMITTERS) 881 | { 882 | if(Editor.emitters[id] == NULL) 883 | { 884 | // allocate memory for emitter 885 | Editor.emitters[id] = calloc(1, sizeof(Emitter)); 886 | if(Editor.emitters[id] == NULL) TraceLog(LOG_FATAL, "EMITTER: Failed to allocate memory"); 887 | Editor.statistics.total_mem += sizeof(Emitter); 888 | } 889 | 890 | if(Editor.emitters[id]->config.forces.data == NULL) { 891 | Editor.emitters[id]->config.forces.data = calloc(MAX_FORCES, sizeof(Force)); 892 | Editor.statistics.total_mem += MAX_FORCES*sizeof(Force); 893 | } 894 | 895 | Emitter* e = Editor.emitters[id]; 896 | int eidx = 0; 897 | idx += bytes; 898 | sscanf(&buffer[idx], " %f %f %04d %f %f %02d %04d %d %02d %f %f", &e->position.x, &e->position.y, &e->particles.max, &e->life, &e->delay, (int*)&e->mode, &eidx, &e->flags, 899 | (int*)&e->config.container.type, &e->config.container.opt1, &e->config.container.opt2); 900 | e->particles.max = MAX_PARTICLES; 901 | // make sure we allocate memory for the particles 902 | if(e->particles.data == NULL) { 903 | e->particles.data = calloc(MAX_PARTICLES, sizeof(Particle)); 904 | Editor.statistics.total_mem += MAX_PARTICLES*sizeof(Particle); 905 | } 906 | if(eidx < SIZEOF(Easings)) e->config.easing = Easings[eidx]; 907 | Editor.emitter_count += 1; 908 | } 909 | } 910 | else if(buffer[0] == 'n') 911 | { 912 | sscanf(buffer, "n %31[^\r\n]s", (char*)&Editor.name); 913 | } 914 | else if(buffer[0] == 'p') 915 | { 916 | char placeholder_path[32] = {0}; 917 | sscanf(buffer, "p %31[^\r\n]s", (char*)&placeholder_path); 918 | 919 | // try to load placeholder 920 | if(strlen((char*)&placeholder_path) > 0) 921 | { 922 | Texture t = LoadTexture(TextFormat("%s/%s", GetDirectoryPath(file), placeholder_path)); 923 | if(t.id > 0) { 924 | UnloadTexture(Editor.placeholder); 925 | Editor.placeholder = t; 926 | Editor.options.show_placeholder = true; 927 | } 928 | } 929 | } 930 | 931 | fgets(buffer, MAX_BUFFER_SIZE, fp); 932 | } 933 | fclose(fp); 934 | 935 | return 1; 936 | } 937 | -------------------------------------------------------------------------------- /particles.h: -------------------------------------------------------------------------------- 1 | /* ========================================================================= 2 | A particle library for raylib (https://github.com/raysan5/raylib) to be 3 | used as a header only library. 4 | WARNING: This is a very early alpha version so take care when using. 5 | 6 | Make sure to #define LIB_RAY_PARTICLES_IMPL in exactly one source file to 7 | include the implementation. 8 | 9 | So far i haven't decided on a stable API so only 2 functions are exposed 10 | ========================================================================= 11 | LICENSE: zlib 12 | Copyright (C) 2021 Vlad Adrian (@Demizdor - https://github.com/Demizdor) 13 | ========================================================================= 14 | */ 15 | 16 | 17 | #pragma once 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | 26 | #define FLAG_SET(n, f) ((n) |= (f)) 27 | #define FLAG_CLEAR(n, f) ((n) &= ~(f)) 28 | #define FLAG_TOGGLE(n, f) ((n) ^= (f)) 29 | #define FLAG_CHECK(n, f) ((n) & (f)) 30 | 31 | typedef struct { 32 | Vector2 origin; // Position of the particle when spawned 33 | Vector2 direction; // Direction vector normalized 34 | Vector2 position; // Current position 35 | float size; // Initial size 36 | float speed; // Initial speed 37 | float time; // Curent particle age 38 | float life; // Total life 39 | float angle; // Starting angle (information is stored in the directional vector but needed by EMITTER_FLAG_DIRECTIONAL_ROTATION) 40 | int tidx; // Index in a multitexture (used only when EMITTER_FLAG_MULTITEXTURE is set) 41 | 42 | // FIXME: the struct is getting way too big (3*8 + 6*4 = 48 bytes) 43 | // think of a way to make it smaller while still retaining all the fields 44 | // maybe calculate the direction each frame (since we already have the angle) 45 | // and just use a random seed when spawned and calculate (mostly) everything from that seed, something like below 46 | // struct { Vector2 origin, position; float time; int seed; } Particle; // (2*8+2*4 = 24 bytes, much better) 47 | } Particle; 48 | 49 | typedef float (*Easing)(float, float, float, float); 50 | 51 | typedef enum { 52 | EMITTER_POINT = 0, 53 | EMITTER_RECT, 54 | EMITTER_CIRCLE, 55 | EMITTER_RING, 56 | //TODO: add an EMITTER_ELIPSE and maybe EMITTER_TRI 57 | } EmitterType; 58 | 59 | typedef enum { 60 | EMITTER_FLAG_DISABLED = 1 << 0, // Is the emitter disabled (default is false) 61 | EMITTER_FLAG_PAUSED = 1 << 1, // Is the emitter currently paused? (default is false) 62 | EMITTER_FLAG_SPAWN_INSIDE = 1 << 2, // Spawn particles inside or outside container (default outside) (used for everything except EMITTER_POINT) 63 | EMITTER_FLAG_REVERSE_DRAW_ORDER = 1 << 3, // Draw particles new -> old or old -> new(default) 64 | EMITTER_FLAG_WORLD_SPACE = 1 << 4, // Particle has origin in local space or world space(default) 65 | EMITTER_FLAG_DRAW_TRIANGLES = 1 << 5, // When no texture is present draw triangles or squares(default) 66 | EMITTER_FLAG_DRAW_OUTLINE = 1 << 6, // When no texture is present draw outlines instead of filled shapes(default) 67 | EMITTER_FLAG_DIRECTIONAL_ROTATION = 1 << 7, // Rotate texture in the direction of movement or not (default) 68 | EMITTER_FLAG_LOOP = 1 << 8, // Stop emitting when dead(default) or loop from start 69 | EMITTER_FLAG_MULTITEXTURE = 1 << 9, // Is the main texture a multitexture or not (default) 70 | } EmitterFlags; 71 | 72 | typedef struct { 73 | float direction; // Direction of the force in degrees 74 | float strength; // Strength (positive or negative) 75 | } Force; 76 | 77 | typedef struct { 78 | int emission; // Maximum number of particles to spawn, when this is reached no particles will spawn until some die 79 | int pulses; 80 | 81 | struct { 82 | float min, max; 83 | } size, // A random size will be selected when spawned 84 | angle, // Restrict particles spawn only between these angles 85 | age, // A random age will be selected for each particle 86 | offset, // A random offset applied to the particle origin 87 | speed; // A random speed will be selected for each particle 88 | 89 | struct { 90 | float start, end; 91 | } scale, // Multiplier applied to the size of the particle 92 | acc, // Linear acceleration 93 | tacc, // Tangential acceleration 94 | rotation; 95 | 96 | struct { 97 | Force* data; // Array holding all the forces affecting the emitter 98 | int count; // Number of forces active on the emitter 99 | } forces; 100 | 101 | struct { 102 | Texture2D texture; // Main texture 103 | int hframes, vframes; // How many horizontal/vertical frames there are (or multitextures when EMITTER_FLAG_MULTITEXTURE is set) 104 | int loop; // How many times to loop the animation (1 -> n) 105 | } atlas; 106 | 107 | struct { 108 | Color* colors; 109 | int count; 110 | } gradient; // Color gradient that will be applied to all particles over time 111 | 112 | Easing easing; // Easing used when updating particles each frame 113 | 114 | struct { 115 | EmitterType type; // Type of the emitter container 116 | float opt1; // First option (depending on the container type) 117 | float opt2; // Second option (depending on the container type) 118 | } container; 119 | 120 | } EmitterConfig; 121 | 122 | 123 | typedef struct { 124 | Vector2 position; 125 | 126 | EmitterConfig config; // Main configuration for the emitter 127 | 128 | struct { 129 | Particle* data; // Array of particles for this emitter (shouldn't be NULL) 130 | int count; // Number of particles that are alive 131 | int max; // Max capacity of the array 132 | } particles; 133 | 134 | float life; // Life of the emitter in seconds 135 | float delay; // How long to wait until the emitter emits particles after it is dead (only if EMITTER_FLAG_LOOP is set) 136 | BlendMode mode; // Emitter blend mode (currently only the raylib blend modes are supported) 137 | int flags; 138 | 139 | // PRIVATE MEMBERS - SHOULDN'T BE CHANGED BY THE USER 140 | float spawn_timer; // Time since last spawned particle 141 | float emit_timer; // Time since emitting particles 142 | 143 | } Emitter; 144 | 145 | // Extra params used when drawing the emitter 146 | typedef struct { 147 | Rectangle* screen; // If not null particles will drawn only if inside this screen area (used to cull particles) 148 | int drawn; // Number of particles drawned each frame 149 | unsigned long long pixels; // Number of pixels drawn each frame 150 | } EmitterExtraParams; 151 | 152 | 153 | // Update emitter `e`. should be called before `EmitterDraw()` 154 | extern int EmitterUpdate(Emitter* e); 155 | // Draw emitter `e` using some extra params. Should be called after `EmitterUpdate()` 156 | extern void EmitterDraw(Emitter* e, EmitterExtraParams* params); 157 | // Get a random float between 0.0 and 1.0 158 | extern float GetRandomFloat(); 159 | // Get a random float between min and max 160 | extern float GetRandomFloatBetween(float min, float max); 161 | 162 | 163 | 164 | 165 | // define this in exactly one source file to include the implementation 166 | // #define LIB_RAY_PARTICLES_IMPL 167 | 168 | #ifdef LIB_RAY_PARTICLES_IMPL 169 | 170 | // Get a random float between 0.0 and 1.0 171 | inline float GetRandomFloat() { 172 | return (float)GetRandomValue(0, RAND_MAX)/RAND_MAX; 173 | } 174 | 175 | // Get a random float between min and max 176 | inline float GetRandomFloatBetween(float min, float max) { 177 | return min + (max - min)*GetRandomFloat(); 178 | } 179 | 180 | // Rotates point `p` `a` degrees around origin `o` 181 | static inline Vector2 RotatePointOnCircle(Vector2 o, Vector2 p, float a) { 182 | const float ra = a*DEG2RAD; 183 | const float s = sin(ra); 184 | const float c = cos(ra); 185 | return (Vector2){o.x + (p.x-o.x)*c - (p.y-o.y)*s, o.y + (p.x-o.x)*s + (p.y-o.y)*c }; 186 | } 187 | 188 | static inline void ParticleUpdate(Emitter* e, Particle* p) { 189 | float dt = GetFrameTime(); 190 | if(dt == 0.0f) dt = 0.0016f; 191 | 192 | const Easing easing = e->config.easing; 193 | 194 | // calculate speed and acceleration 195 | const float speed = (p->speed + easing(p->time, e->config.acc.start, e->config.acc.end - e->config.acc.start, p->life))*dt; 196 | Vector2 npos = Vector2Add(p->position, Vector2Scale(p->direction, speed)); 197 | 198 | // calculate tangential acceleration 199 | const float tacc = easing(p->time, e->config.tacc.start, e->config.tacc.end - e->config.tacc.start, p->life)*dt; 200 | if(tacc != 0.0f) { 201 | Vector2 n = Vector2Subtract(npos, p->origin); 202 | float angle = 90.0f; 203 | if(tacc < 0.0f) { 204 | n = Vector2Subtract(p->origin, npos); 205 | angle = -90.0f; 206 | } 207 | 208 | n = Vector2Normalize(n); 209 | //#define MIN_RADIUS 20.0f 210 | //Vector2 t = Vector2Add(npos, Vector2Scale(n, tacc*(Vector2Distance(npos, p->origin)/MIN_RADIUS) )); 211 | // FIXME: `tacc` needs to depend on the distance to origin (larger distance -> bigger effect) 212 | Vector2 t = Vector2Add(npos, Vector2Scale(n, tacc)); 213 | p->position = RotatePointOnCircle(npos, t, angle); 214 | } 215 | else p->position = npos; 216 | 217 | // TODO: just adding the forces together..hmmm, is this correct?! 218 | Vector2 forces = {0.0f, 0.0f}; 219 | for(int i=0; iconfig.forces.count; ++i) { 220 | Force* f = &e->config.forces.data[i]; 221 | const float angle = f->direction*DEG2RAD; 222 | Vector2 direction = Vector2Normalize((Vector2){cosf(angle), sinf(angle)}); 223 | forces = Vector2Add(forces, Vector2Scale(direction, f->strength*dt)); 224 | } 225 | 226 | p->position = Vector2Add(p->position, forces); 227 | 228 | p->time += dt; 229 | } 230 | 231 | // Keep angle between 0-360 232 | static inline float NormalizeAngle(float angle) { 233 | angle = fmodf(angle, 360.0); 234 | if (angle < 0.0f) angle += 360.0; 235 | return angle; 236 | } 237 | 238 | // Get quadrant from angle (0-7 quadrants each 45 degrees) 239 | static inline int GetQuadrant(float angle) { 240 | return (int)fmodf(NormalizeAngle(angle)/45.0f, 8.0); 241 | } 242 | 243 | static Particle ParticleGenerate(Emitter* e) { 244 | Particle p; 245 | 246 | // Get offset and angle 247 | const Vector2 offset = {GetRandomFloatBetween(e->config.offset.min, e->config.offset.max), 248 | GetRandomFloatBetween(e->config.offset.min, e->config.offset.max)}; 249 | p.angle = GetRandomFloatBetween(e->config.angle.min, e->config.angle.max); 250 | const float angle = p.angle*DEG2RAD; 251 | 252 | // Generate a random multitexture index if EMITTER_FLAG_MULTITEXTURE is set 253 | if(e->config.atlas.hframes*e->config.atlas.vframes > 1) p.tidx = GetRandomValue(0, e->config.atlas.hframes*e->config.atlas.vframes-1); 254 | 255 | switch(e->config.container.type) 256 | { 257 | case EMITTER_POINT: 258 | p.origin = offset; 259 | p.direction = Vector2Normalize((Vector2){cosf(angle), sinf(angle)}); 260 | break; 261 | 262 | case EMITTER_RECT: { 263 | const float width = e->config.container.opt1; 264 | const float height = e->config.container.opt2; 265 | 266 | if(!FLAG_CHECK(e->flags, EMITTER_FLAG_SPAWN_INSIDE)) { 267 | // spawn outside the rectangle (is there a better way to do this?!?) 268 | const float a = angle*RAD2DEG; 269 | const int q = GetQuadrant(a); // divide the rectangle in 8(0-7) quadrants 45degrees each 270 | const float pc = fmodf(NormalizeAngle(a), 45.0f) / 45.0f; // calculate percent of the side to fill 271 | // calculate x/y coordinates for each quadrant 272 | const float qw[] = { width/2, (width/2)*(1.0-pc), (-width/2)*pc, -width/2, -width/2, (-width/2)*(1.0-pc), (width/2)*pc, width/2}; 273 | const float qh[] = { (height/2)*pc, height/2, height/2, (height/2)*(1.0-pc), (-height/2)*pc, -height/2, -height/2, (-height/2)*(1.0-pc)}; 274 | // set coordinates depending on quadrant 275 | p.origin.x = qw[q]; 276 | p.origin.y = qh[q]; 277 | } else { 278 | // randomly spawn inside the rectangle 279 | p.origin.x = GetRandomValue(-width/2, width/2); 280 | p.origin.y = GetRandomValue(-height/2, height/2); 281 | } 282 | p.direction = Vector2Normalize((Vector2){width*cosf(angle), height*sinf(angle)}); 283 | p.origin = Vector2Add(p.origin, offset); 284 | } 285 | break; 286 | 287 | case EMITTER_CIRCLE: 288 | { 289 | const float r = e->config.container.opt1; 290 | p.direction = Vector2Normalize((Vector2){r*cosf(angle), r*sinf(angle)}); 291 | if(!FLAG_CHECK(e->flags, EMITTER_FLAG_SPAWN_INSIDE)) p.origin = Vector2Scale(p.direction, r); // spawn outside the circle 292 | else p.origin = Vector2Scale(p.direction, GetRandomFloatBetween(0.0f, r)); // spawn inside circle 293 | p.origin = Vector2Add(p.origin, offset); 294 | } 295 | break; 296 | 297 | case EMITTER_RING: 298 | { 299 | float ra = e->config.container.opt1; 300 | float rb = e->config.container.opt2; 301 | 302 | if(ra > rb) { // swap if needed 303 | const float tmp = ra; 304 | ra = rb; 305 | rb = tmp; 306 | } 307 | 308 | p.direction = Vector2Normalize((Vector2){rb*cosf(angle), rb*sinf(angle)}); 309 | if(!FLAG_CHECK(e->flags, EMITTER_FLAG_SPAWN_INSIDE)) { 310 | const float rnd = GetRandomFloat(); // get a random float to see where should we spawn 311 | if(rnd >= 0.5f) 312 | p.origin = Vector2Scale(p.direction, rb); // spawn outside outer ring 313 | else { 314 | p.origin = Vector2Scale(p.direction, ra); // spawn inside inner ring 315 | p.direction = Vector2Negate(p.direction); // inverse direction 316 | p.angle *= -1.0f; 317 | } 318 | } else { 319 | p.origin = Vector2Scale(p.direction, GetRandomFloatBetween(ra, rb)); // spawn inbetween rings 320 | } 321 | p.origin = Vector2Add(p.origin, offset); 322 | } 323 | break; 324 | } 325 | 326 | if(!FLAG_CHECK(e->flags, EMITTER_FLAG_WORLD_SPACE)) { 327 | p.origin.x += e->position.x; 328 | p.origin.y += e->position.y; 329 | } 330 | p.position = p.origin; 331 | 332 | // Calculate initial particle size and speed 333 | p.size = GetRandomFloatBetween(e->config.size.min, e->config.size.max); 334 | p.speed = GetRandomFloatBetween(e->config.speed.min, e->config.speed.max); 335 | 336 | // Calculate particle life 337 | p.life = GetRandomFloatBetween(e->config.age.min, e->config.age.max); 338 | p.time = 0.0f; 339 | 340 | return p; 341 | } 342 | 343 | int EmitterUpdate(Emitter* e) { 344 | if(FLAG_CHECK(e->flags, EMITTER_FLAG_DISABLED) || FLAG_CHECK(e->flags, EMITTER_FLAG_PAUSED)) 345 | return 0; // don't update when paused or disabled 346 | 347 | // Emit particles 348 | if(e->particles.count < e->config.emission && e->life != 0.0f && e->emit_timer > e->delay && e->emit_timer < e->delay + e->life) 349 | { 350 | const float duration = e->life; 351 | const float dt = GetFrameTime(); 352 | 353 | // FIXME: hmmm... this is wrong!!! not all the particles are emitted. 354 | float tick = dt; 355 | int rate = (float)e->config.emission/duration*dt; 356 | 357 | if(e->config.pulses != 0) { 358 | rate = e->config.emission/e->config.pulses; // rate per pulse 359 | tick = duration/e->config.pulses; // time of each pulse 360 | } 361 | 362 | if(e->particles.count == 0) tick = e->spawn_timer; 363 | 364 | if(rate + e->particles.count > e->config.emission) rate = e->config.emission - e->particles.count; 365 | if(e->spawn_timer >= tick) 366 | { 367 | e->spawn_timer -= tick; 368 | for(int i=0, r=0; iparticles.max; ++i) { 369 | if(e->particles.data[i].life == 0.0f) { 370 | e->particles.data[i] = ParticleGenerate(e); 371 | e->particles.count += 1; 372 | if(++r >= rate) break; 373 | } 374 | } 375 | } 376 | e->spawn_timer += dt; 377 | } 378 | 379 | // Update particles 380 | int updated = 0; 381 | for(int i=0; iparticles.max && e->particles.count > 0; ++i) { 382 | if(e->particles.data[i].life != 0.0f) { 383 | if(e->particles.data[i].time >= e->particles.data[i].life) { 384 | // remove particle 385 | e->particles.count -= 1; 386 | if(e->particles.count < 0 ) e->particles.count = 0; 387 | e->particles.data[i].life = 0.0f; 388 | } else { 389 | ParticleUpdate(e, &e->particles.data[i]); 390 | if(i > 0 && e->particles.data[i-1].life > e->particles.data[i].life) 391 | { 392 | // overtime swapping values like this will autosort the particle array (over time) 393 | Particle tmp = e->particles.data[i]; 394 | e->particles.data[i] = e->particles.data[i-1]; 395 | e->particles.data[i-1] = tmp; 396 | } 397 | ++updated; 398 | } 399 | } 400 | } 401 | 402 | // Handle emitting in a loop 403 | if(FLAG_CHECK(e->flags, EMITTER_FLAG_LOOP)) 404 | { 405 | if(e->emit_timer > 2*e->delay + e->life) { e->emit_timer = e->delay; } 406 | else { e->emit_timer += GetFrameTime(); } 407 | } 408 | else 409 | { 410 | if(e->emit_timer < 2*e->delay + e->life) 411 | e->emit_timer += GetFrameTime(); 412 | } 413 | 414 | return updated; 415 | } 416 | 417 | static inline Color MixColors(Emitter* e, Color a, Color b, float st, float et) { 418 | int cr = e->config.easing(st, a.r, b.r-a.r, et); 419 | cr = Clamp(cr, 0, 255); 420 | int cg = e->config.easing(st, a.g, b.g-a.g, et); 421 | cg = Clamp(cg, 0, 255); 422 | int cb = e->config.easing(st, a.b, b.b-a.b, et); 423 | cb = Clamp(cb, 0, 255); 424 | 425 | Color color = {cr, cg, cb, b.a}; 426 | if(b.a != a.a) 427 | { 428 | int ca = e->config.easing(st, a.a, b.a-a.a, et); 429 | color.a = Clamp(ca, 0, 255); 430 | } 431 | 432 | return color; 433 | } 434 | 435 | static inline Color Interpolate(Emitter* e, Particle* p) { 436 | if(e->config.gradient.colors == NULL || e->config.gradient.count == 0) return RED; 437 | if(e->config.gradient.count == 1) return e->config.gradient.colors[0]; // no need to interpolate since there's only one color 438 | 439 | const int max = e->config.gradient.count - 1; 440 | const float u = p->life/max; 441 | int idx = 0; 442 | idx = floorf(p->time*max/p->life); 443 | 444 | float st = fmodf(p->time, u); 445 | if(st+GetFrameTime() > u) st = u; 446 | 447 | return MixColors(e, e->config.gradient.colors[idx], e->config.gradient.colors[idx+1], st, u); 448 | } 449 | 450 | void EmitterDraw(Emitter* e, EmitterExtraParams* params) 451 | { 452 | // NOTE: this code has been (somewhat) optimised but still slow :( 453 | if(!FLAG_CHECK(e->flags, EMITTER_FLAG_DISABLED) && e->particles.count > 0) // don't draw when disabled 454 | { 455 | BeginBlendMode(e->mode); 456 | int start = 0, end = e->particles.max, step = 1; 457 | if(FLAG_CHECK(e->flags, EMITTER_FLAG_REVERSE_DRAW_ORDER)) 458 | { 459 | // drawn particles in reverse order 460 | start = e->particles.max - 1; 461 | end = -1; 462 | step = -1; 463 | } 464 | 465 | if(e->config.atlas.texture.id != 0) 466 | { 467 | // DRAW TEXTURED PARTICLES 468 | for(int i=start; i!=end; i+=step) 469 | { 470 | Particle* p = &e->particles.data[i]; 471 | if(p->life != 0.0f) 472 | { 473 | float size = p->size*e->config.easing(p->time, e->config.scale.start, e->config.scale.end - e->config.scale.start, p->life); 474 | 475 | float rotst = e->config.rotation.start; 476 | if(FLAG_CHECK(e->flags, EMITTER_FLAG_DIRECTIONAL_ROTATION)) rotst += p->angle; 477 | float rotation = e->config.easing(p->time, rotst, e->config.rotation.end - e->config.scale.start, p->life); 478 | 479 | Vector2 center = p->position; 480 | if(FLAG_CHECK(e->flags, EMITTER_FLAG_WORLD_SPACE)) center = Vector2Add(center, e->position); 481 | 482 | float w = (float)e->config.atlas.texture.width*size; 483 | float h = (float)e->config.atlas.texture.height*size; 484 | if(e->config.atlas.hframes*e->config.atlas.vframes > 1) { 485 | w = (float)e->config.atlas.texture.width/e->config.atlas.hframes*size; 486 | h = (float)e->config.atlas.texture.height/e->config.atlas.vframes*size; 487 | } 488 | Vector2 vertex[4] = { (Vector2){center.x-w/2, center.y-h/2}, 489 | (Vector2){center.x+w/2, center.y-h/2}, 490 | (Vector2){center.x+w/2, center.y+h/2}, 491 | (Vector2){center.x-w/2, center.y+h/2}, 492 | }; 493 | 494 | if(rotation != 0.0f) { 495 | // rotate the vertices of the quad that holds the texture 496 | vertex[0] = RotatePointOnCircle(center, vertex[0], rotation); 497 | vertex[1] = RotatePointOnCircle(center, vertex[1], rotation); 498 | vertex[2] = RotatePointOnCircle(center, vertex[2], rotation); 499 | vertex[3] = RotatePointOnCircle(center, vertex[3], rotation); 500 | } 501 | 502 | int inside = true; 503 | if(params->screen != NULL) 504 | { 505 | // check rotated points to see if at least one is inside the screen area 506 | inside = CheckCollisionPointRec(vertex[0], *params->screen) | CheckCollisionPointRec(vertex[1], *params->screen) | 507 | CheckCollisionPointRec(vertex[2], *params->screen) | CheckCollisionPointRec(vertex[3], *params->screen); 508 | } 509 | 510 | if(inside) 511 | { 512 | // Get current color by interpolating 513 | Color color = Interpolate(e, p); 514 | 515 | if(e->config.atlas.hframes*e->config.atlas.vframes <= 1) 516 | { 517 | // DRAW STATIC TEXTURE 518 | DrawTexturePro(e->config.atlas.texture, (Rectangle){0.0f, 0.0f, e->config.atlas.texture.width, e->config.atlas.texture.height}, 519 | (Rectangle){vertex[0].x, vertex[0].y, w, h}, (Vector2){0.0f, 0.0f}, rotation, color); 520 | } 521 | else 522 | { 523 | // DRAW ANIMATED TEXTURE OR MULTITEXTURED PARTICLES 524 | int frame = p->tidx; // set multitexture index 525 | if(!FLAG_CHECK(e->flags, EMITTER_FLAG_MULTITEXTURE)) { 526 | // This is a animated texture so get the current frame of animation 527 | frame = e->config.easing(p->time, 0, e->config.atlas.vframes*e->config.atlas.hframes*e->config.atlas.loop-1, p->life); 528 | frame = Clamp(frame, 0.0f, e->config.atlas.vframes*e->config.atlas.hframes*e->config.atlas.loop-1); 529 | } 530 | 531 | Rectangle src = {0.0f, 0.0f, 532 | (float)e->config.atlas.texture.width/e->config.atlas.hframes, 533 | (float)e->config.atlas.texture.height/e->config.atlas.vframes 534 | }; 535 | 536 | src.x = (frame%e->config.atlas.hframes)*src.width; 537 | src.y = ((int)floorf(frame/e->config.atlas.hframes)%e->config.atlas.vframes)*src.height; 538 | DrawTexturePro(e->config.atlas.texture, src, (Rectangle){vertex[0].x, vertex[0].y, w, h}, (Vector2){0.0f, 0.0f}, rotation, color); 539 | } 540 | params->pixels += w*h; 541 | params->drawn++; 542 | } 543 | /* draw bounding box 544 | DrawLineEx(vertex[0], vertex[1], 2.0f, RED); 545 | DrawLineEx(vertex[1], vertex[2], 2.0f, RED); 546 | DrawLineEx(vertex[2], vertex[3], 2.0f, RED); 547 | DrawLineEx(vertex[3], vertex[0], 2.0f, RED); 548 | */ 549 | } 550 | } 551 | } 552 | else 553 | { 554 | // DRAW UNTEXTURED PARTICLES 555 | for(int i=start; i!=end; i+=step) 556 | { 557 | Particle* p = &e->particles.data[i]; 558 | 559 | if(p->life != 0.0f) 560 | { 561 | float size = p->size*e->config.easing(p->time, e->config.scale.start, e->config.scale.end - e->config.scale.start, p->life); 562 | float rotation = e->config.easing(p->time, e->config.rotation.start, e->config.rotation.end - e->config.rotation.start, p->life); 563 | Color color = Interpolate(e, p); 564 | 565 | Vector2 center = p->position; 566 | if(FLAG_CHECK(e->flags, EMITTER_FLAG_WORLD_SPACE)) center = Vector2Add(center, e->position); 567 | 568 | if(!FLAG_CHECK(e->flags, EMITTER_FLAG_DRAW_TRIANGLES)) 569 | { 570 | // DRAW SQUARES 571 | Vector2 point[5] = { (Vector2){center.x-size/2, center.y-size/2}, 572 | (Vector2){center.x+size/2, center.y-size/2}, 573 | (Vector2){center.x+size/2, center.y+size/2}, 574 | (Vector2){center.x-size/2, center.y+size/2}, 575 | (Vector2){center.x-size/2, center.y-size/2} 576 | }; 577 | 578 | if(rotation != 0.0f) { 579 | // rotate the points of the rectangle 580 | point[0] = RotatePointOnCircle(center, point[0], rotation); 581 | point[1] = RotatePointOnCircle(center, point[1], rotation); 582 | point[2] = RotatePointOnCircle(center, point[2], rotation); 583 | point[3] = RotatePointOnCircle(center, point[3], rotation); 584 | point[4] = point[0]; // the rect has only 4 points, the 5th point is just used by DrawLineStrip() 585 | } 586 | 587 | int inside = true; 588 | 589 | if(params->screen != NULL) { 590 | // check rotated points to see if at least one is inside the screen area 591 | inside = CheckCollisionPointRec(point[0], *params->screen) | CheckCollisionPointRec(point[1], *params->screen) | 592 | CheckCollisionPointRec(point[2], *params->screen) | CheckCollisionPointRec(point[3], *params->screen); 593 | } 594 | 595 | if(inside) 596 | { 597 | if(!FLAG_CHECK(e->flags, EMITTER_FLAG_DRAW_OUTLINE)) { 598 | DrawRectanglePro((Rectangle){point[0].x, point[0].y, size, size}, (Vector2){0.0f, 0.0f}, rotation, color); 599 | params->pixels += size*size; 600 | } 601 | else { 602 | DrawLineStrip((Vector2*)&point, 5, color); 603 | params->pixels += 2*(size+size); 604 | } 605 | params->drawn++; 606 | } 607 | } 608 | else 609 | { 610 | // DRAW TRIANGLES 611 | Vector2 point[4] = {(Vector2){center.x, center.y-size/2}, 612 | (Vector2){center.x+size/2, center.y+size/2}, 613 | (Vector2){center.x-size/2, center.y+size/2}, 614 | (Vector2){center.x, center.y-size/2}, 615 | }; 616 | 617 | if(rotation != 0.0f) { 618 | // rotate the points of the rectangle 619 | point[0] = RotatePointOnCircle(center, point[0], rotation); 620 | point[1] = RotatePointOnCircle(center, point[1], rotation); 621 | point[2] = RotatePointOnCircle(center, point[2], rotation); 622 | point[3] = point[0]; // the triangle has only 3 points, the 4th point is just used by DrawLineStrip() 623 | } 624 | 625 | int inside = true; 626 | if(params->screen != NULL) { 627 | // check rotated points to see if at least one is inside the screen area 628 | inside = CheckCollisionPointRec(point[0], *params->screen) | CheckCollisionPointRec(point[1], *params->screen) | 629 | CheckCollisionPointRec(point[2], *params->screen); 630 | } 631 | 632 | if(inside) { 633 | if(!FLAG_CHECK(e->flags, EMITTER_FLAG_DRAW_OUTLINE)) { 634 | DrawTriangle(point[0], point[2], point[1], color); 635 | params->pixels += (size*size*sqrtf(3))/4; 636 | } 637 | else { 638 | DrawLineStrip((Vector2*)&point, 4, color); 639 | params->pixels += 3*size; 640 | } 641 | 642 | params->drawn++; 643 | } 644 | 645 | } 646 | } 647 | } 648 | } 649 | EndBlendMode(); 650 | } 651 | } 652 | 653 | 654 | #endif // LIB_RAY_PARTICLES_IMPL -------------------------------------------------------------------------------- /screenshot/screenshot000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Demizdor/particle_editor/9ef860173534e6a9455495a36002ae390e84fc11/screenshot/screenshot000.png --------------------------------------------------------------------------------