├── 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
--------------------------------------------------------------------------------