Просмотр исходного кода

Added a new example: animation curves (#268)

* A somewhat advanced example with custom curve controls.

* Name update

* typos

* comments
Pierre Jaffuer 2 лет назад
Родитель
Сommit
1d9fd31074

+ 2 - 1
examples/Makefile

@@ -354,7 +354,8 @@ EXAMPLES = \
     portable_window/portable_window \
     scroll_panel/scroll_panel \
     style_selector/style_selector \
-    custom_sliders/custom_sliders
+    custom_sliders/custom_sliders \
+    animation_curve/animation_curve
 
 CURRENT_MAKEFILE = $(lastword $(MAKEFILE_LIST))
 

+ 480 - 0
examples/animation_curve/animation_curve.c

@@ -0,0 +1,480 @@
+/*******************************************************************************************
+*
+*   Animation curves - An example demo for animation curves
+*
+*   DEPENDENCIES:
+*       raylib 4.0  - Windowing/input management and drawing.
+*       raygui 3.0  - Immediate-mode GUI controls.
+*
+*   COMPILATION (Windows - MinGW):
+*       gcc -o $(NAME_PART).exe $(FILE_NAME) -I../../src -lraylib -lopengl32 -lgdi32 -std=c99
+*
+*   LICENSE: zlib/libpng
+*
+*   Copyright (c) 2023 Pierre Jaffuer (@smallcluster)
+*
+**********************************************************************************************/
+
+#include "raylib.h"
+
+#define RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT 24
+#define RAYGUI_IMPLEMENTATION
+#include "../../src/raygui.h"
+
+// raygui embedded styles
+#include "../style_selector/styles/style_cyber.h"             // raygui style: cyber
+#include "../style_selector/styles/style_jungle.h"            // raygui style: jungle
+#include "../style_selector/styles/style_lavanda.h"           // raygui style: lavanda
+#include "../style_selector/styles/style_dark.h"              // raygui style: dark
+#include "../style_selector/styles/style_bluish.h"            // raygui style: bluish
+#include "../style_selector/styles/style_terminal.h"          // raygui style: terminal
+
+#undef RAYGUI_IMPLEMENTATION            // Avoid including raygui implementation again
+
+#define GUI_CURVE_EDIT_IMPLEMENTATION
+#include "gui_curve_edit.h"
+
+//------------------------------------------------------------------------------------
+// Helper function
+//------------------------------------------------------------------------------------
+
+void LoadDefaults(GuiCurveEditState curves[]);
+
+//------------------------------------------------------------------------------------
+// Program main entry point
+//------------------------------------------------------------------------------------
+int main()
+{
+    // Initialization
+    //---------------------------------------------------------------------------------------
+    const int screenWidth = 800;
+    const int screenHeight = 540;
+
+    InitWindow(screenWidth, screenHeight, "Animation curves");
+    SetTargetFPS(60);
+
+    // Gui style
+    GuiLoadStyleDefault();
+    int visualStyleActive = 0;
+    int prevVisualStyleActive = 0;
+
+    float fontSize = GuiGetStyle(DEFAULT, TEXT_SIZE);
+    const float margin = 8;
+
+    // Gui states
+    Vector2 scrollOffset = (Vector2) {0,0};
+    Rectangle contentRect = (Rectangle) {0,0,0,0};
+    bool moveSlider = false;
+    bool sectionActive[5] = {false};
+    sectionActive[0] = true;
+    const char* sectionNames[5] = {"X Position", "Y Position", "Width", "Height", "Rotation"};
+    bool editValueBox[5][4] = {false};
+    char* valTextBox[5][4][20];
+    bool playAnimation = true;
+    bool showHelp = true;
+
+    Rectangle settingsRect = (Rectangle) {screenWidth-screenWidth/3, 0, screenWidth/3, screenHeight};
+    
+    // Animation curves
+    // 0 -> Ball X position
+    // 1 -> Ball Y position
+    // 2 -> Ball Width
+    // 3 -> Ball Height
+    // 4 -> Ball rotation
+    GuiCurveEditState curves[5];
+    LoadDefaults(curves);
+    
+    // Animation time
+    float time = 0.f;
+    float animationTime = 4.f;
+
+    //--------------------------------------------------------------------------------------
+
+    // Main game loop
+    while (!WindowShouldClose())    // Detect window close button or ESC key
+    {
+        // Update
+        //----------------------------------------------------------------------------------
+
+        if(playAnimation)
+            time += GetFrameTime();
+
+        // Reset timer
+        if(time > animationTime)
+            time = 0;
+
+        // Ball animation
+        const float t = time / animationTime;
+        Vector2 ballPos = (Vector2) {EvalGuiCurve(&curves[0], t), EvalGuiCurve(&curves[1], t)};
+        Vector2 ballSize = (Vector2) {EvalGuiCurve(&curves[2], t), EvalGuiCurve(&curves[3], t)};
+        float ballRotation = EvalGuiCurve(&curves[4], t);
+
+        // Update style
+        if(visualStyleActive != prevVisualStyleActive){
+            switch (visualStyleActive)
+            {
+                case 0: GuiLoadStyleDefault(); break;
+                case 1: GuiLoadStyleJungle(); break;
+                case 2: GuiLoadStyleLavanda(); break;
+                case 3: GuiLoadStyleDark(); break;
+                case 4: GuiLoadStyleBluish(); break;
+                case 5: GuiLoadStyleCyber(); break;
+                case 6: GuiLoadStyleTerminal(); break;
+                default: break;
+            }
+            fontSize = GuiGetStyle(DEFAULT, TEXT_SIZE);
+            prevVisualStyleActive = visualStyleActive;
+        }
+
+        // Update settings panel rect
+        Rectangle sliderRect = (Rectangle) {settingsRect.x-4, settingsRect.y, 4, settingsRect.height};
+        if(CheckCollisionPointRec(GetMousePosition(), sliderRect) && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)){
+            moveSlider = true;
+        } 
+        if(IsMouseButtonUp(MOUSE_BUTTON_LEFT)){
+            moveSlider = false;
+        }
+        if(moveSlider){
+            settingsRect.x = GetMouseX();
+            // Minimum-Maximum size
+            if(settingsRect.x > screenWidth-4)
+                settingsRect.x = screenWidth-4;
+            else if(settingsRect.x < 4) // width of the slider
+                settingsRect.x = 4;
+            settingsRect.width = screenWidth-settingsRect.x;
+        }
+
+
+        //----------------------------------------------------------------------------------
+
+        // Draw
+        //----------------------------------------------------------------------------------
+        BeginDrawing();
+            ClearBackground(GetColor( GuiGetStyle(DEFAULT, BACKGROUND_COLOR)));
+
+            // Scene
+            //----------------------------------------------------------------------------------
+
+            // sky
+            DrawRectangle(curves[0].start, curves[1].end, curves[0].end-curves[0].start, curves[1].start-curves[1].end, BLUE);
+
+            // ground
+            DrawRectangle(curves[0].start, curves[1].start, curves[0].end-curves[0].start, 32, DARKGREEN);
+
+            BeginScissorMode(curves[0].start, curves[1].end, curves[0].end-curves[0].start, curves[1].start-curves[1].end+32);
+                // Draw ball
+                DrawRectanglePro((Rectangle){ballPos.x, ballPos.y, ballSize.x, ballSize.y}, (Vector2){ballSize.x/2.f,ballSize.y/2.f}, ballRotation, PINK);
+                // Local origin
+                DrawLine(ballPos.x, ballPos.y, ballPos.x + cosf(ballRotation*DEG2RAD)*ballSize.x, ballPos.y +sinf(ballRotation*DEG2RAD)*ballSize.y, RED);
+                DrawLine(ballPos.x, ballPos.y, ballPos.x + cosf((ballRotation+90)*DEG2RAD)*ballSize.x, ballPos.y +sinf((ballRotation+90)*DEG2RAD)*ballSize.y, GREEN);
+            EndScissorMode();
+
+            // Bounds
+            DrawRectangleLines(curves[0].start, curves[1].end, curves[0].end-curves[0].start, curves[1].start-curves[1].end+32, GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL)));
+
+            // GUI
+            //----------------------------------------------------------------------------------
+            
+			// Help window
+			if(showHelp){
+                if(GuiWindowBox((Rectangle) {margin, margin, settingsRect.x-2*margin, curves[1].end-2*margin}, "help")){
+                    showHelp = false;
+                }
+                Rectangle helpTextRect = (Rectangle) {2*margin, 2*margin+RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT, settingsRect.x-4-4*margin, 0};
+                GuiLabel((Rectangle) {helpTextRect.x, helpTextRect.y+helpTextRect.height, helpTextRect.width, fontSize}, "Curve widget controls:");
+                helpTextRect.height += fontSize+margin;
+                GuiLabel((Rectangle) {helpTextRect.x, helpTextRect.y+helpTextRect.height, helpTextRect.width, fontSize}, "- Left click to move/add point or move tangents");
+                helpTextRect.height += fontSize+margin/2;
+                GuiLabel((Rectangle) {helpTextRect.x, helpTextRect.y+helpTextRect.height, helpTextRect.width, fontSize}, "- While moving a tangent, hold SHIFT to disable tangent symetry");
+                helpTextRect.height += fontSize+margin/2;
+                GuiLabel((Rectangle) {helpTextRect.x, helpTextRect.y+helpTextRect.height, helpTextRect.width, fontSize}, "- Right click to remove a point");
+                helpTextRect.height += fontSize+margin/2;
+                DrawRectangleGradientV(margin, margin+curves[1].end-2*margin, settingsRect.x-2*margin, 12, (Color){0,0,0,100}, BLANK);
+            } 
+
+            // Settings panel
+            GuiScrollPanel(settingsRect, "Settings", contentRect, &scrollOffset);
+            // Clip rendering
+            BeginScissorMode(settingsRect.x, settingsRect.y+RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT, settingsRect.width, settingsRect.height);
+            // Rebuild the content Rect
+            contentRect = (Rectangle) {settingsRect.x+margin, RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT+margin, settingsRect.width-2*margin-GuiGetStyle(LISTVIEW, SCROLLBAR_WIDTH), 0};
+
+            // Help button
+            if(GuiButton((Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width, 1.5*fontSize}, GuiIconText(showHelp ? ICON_EYE_ON : ICON_EYE_OFF, "Curve controls help"))){
+                showHelp = !showHelp;
+            }
+            contentRect.height += 1.5*fontSize + margin;
+
+            // Animation Time slider
+            animationTime = GuiSlider((Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width/2, fontSize}, NULL, TextFormat("Animation Time: %.2fs", animationTime), animationTime, 1, 8);
+            contentRect.height += fontSize + margin;
+
+            // Load default curves
+            if(GuiButton((Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width, 1.5*fontSize}, "Load default")){
+                LoadDefaults(curves);
+                animationTime = 4.f;
+                time = 0.f;
+            }
+            contentRect.height += 1.5*fontSize + margin;
+
+            // Styles
+            GuiLabel((Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width, fontSize}, "Style:");
+            contentRect.height += fontSize;
+            visualStyleActive = GuiComboBox((Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width, 1.5*fontSize}, "default;Jungle;Lavanda;Dark;Bluish;Cyber;Terminal", visualStyleActive);
+            contentRect.height += 1.5*fontSize + margin;
+
+            // Draw curves with their controls 
+            //----------------------------------------------------------------------------------
+            for(int i=0; i < 5; i++){
+                // Collapsing section
+                Rectangle headerRect = (Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width, 1.5*fontSize};
+                GuiStatusBar(headerRect, NULL);
+                if(GuiLabelButton(headerRect, GuiIconText(sectionActive[i] ? ICON_ARROW_DOWN_FILL : ICON_ARROW_RIGHT_FILL, sectionNames[i]))){
+                    sectionActive[i] = !sectionActive[i];
+                }
+                contentRect.height += 1.5*fontSize + margin;
+                
+                // Skip this section
+                if(!sectionActive[i])
+                    continue;
+
+                // Draw curve control
+                Rectangle curveRect = (Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width, fontSize*12};
+                EndScissorMode(); // Stop clipping from setting rect
+                // Curves can leaks from control boundary... scissor it !
+                BeginScissorMode(curveRect.x, curveRect.y, curveRect.width, curveRect.height);
+                    GuiCurveEdit(&curves[i],curveRect);
+                EndScissorMode();
+                // Resume clipping from setting rect
+                BeginScissorMode(settingsRect.x, settingsRect.y+RAYGUI_WINDOWBOX_STATUSBAR_HEIGHT, settingsRect.width, settingsRect.height);
+                contentRect.height += fontSize*12 + margin;
+                
+                // Draw selected point controls
+                GuiCurveEditPoint* p = &(curves[i].points[curves[i].selectedIndex]);
+                p->leftLinear =  GuiCheckBox((Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, 1.5*fontSize, 1.5*fontSize}, "Left Linear", p->leftLinear);
+                p->rightLinear =  GuiCheckBox((Rectangle){contentRect.x+contentRect.width/2, contentRect.y+contentRect.height+scrollOffset.y, 1.5*fontSize, 1.5*fontSize}, "Right Linear", p->rightLinear);
+                contentRect.height += 1.5*fontSize + margin;
+
+                // Positions
+                GuiLabel((Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width, fontSize}, "Position");
+                contentRect.height += fontSize;
+
+                if(!editValueBox[i][0]){
+                    // transform x position to string
+                    gcvt(p->position.x, 6, valTextBox[i][0]);
+                }
+                if(!editValueBox[i][1]){
+                    // transform y position to string
+                    gcvt(curves[i].start + (curves[i].end-curves[i].start) * p->position.y, 6, valTextBox[i][1]);
+                }
+
+                // X pos
+                if(GuiTextBox((Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width/2-margin, 1.5*fontSize}, valTextBox[i][0], 20, editValueBox[i][0])){
+                    editValueBox[i][0] = !editValueBox[i][0];
+                    // input ended
+                    if(!editValueBox[i][0]){
+                        // Try to convert text to float and assign it to the point
+                        char * endPtr;
+                        double value = strtod( (char *) valTextBox[i][0], &endPtr); 
+                        if ( endPtr != (char *) valTextBox[i][0] ) {
+                            p->position.x = value < 0 ? 0 : value > 1 ? 1 : value;
+                        }
+                    }
+                }
+                // Y pos
+                if(GuiTextBox((Rectangle){contentRect.x+contentRect.width/2, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width/2, 1.5*fontSize}, valTextBox[i][1], 20, editValueBox[i][1])){
+                    editValueBox[i][1] = !editValueBox[i][1];
+
+                    // input ended
+                    if(!editValueBox[i][1]){
+                        // Try to convert text to float and assign it to the point
+                        char * endPtr;
+                        double value = strtod( (char *)valTextBox[i][1], &endPtr); 
+                        if ( endPtr != (char *) valTextBox[i][1] ) {
+                            float normalizedVal = (value-curves[i].start) / (curves[i].end-curves[i].start);
+                            p->position.y = normalizedVal < 0 ? 0 : normalizedVal > 1 ? 1 : normalizedVal;
+                        } 
+                    }
+                    
+                }
+                contentRect.height += 1.5*fontSize + margin;
+
+                // Tangents
+                GuiLabel((Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width, fontSize}, "Tangents");
+                contentRect.height += fontSize;
+
+                if(!editValueBox[i][2]){
+                    // transform left tangent to string
+                    gcvt(p->tangents.x, 6, valTextBox[i][2]);
+                }
+                if(!editValueBox[i][3]){
+                    // transform right tangent to string
+                    gcvt(p->tangents.y, 6, valTextBox[i][3]);
+                }
+
+                // Left tan
+                if(GuiTextBox((Rectangle){contentRect.x, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width/2-margin, 1.5*fontSize}, valTextBox[i][2], 20, editValueBox[i][2])){
+                    editValueBox[i][2] = !editValueBox[i][2];
+                    // input ended
+                    if(!editValueBox[i][2]){
+                        // Try to convert text to float and assign it to the point
+                        char * endPtr;
+                        double value = strtod( (char *) valTextBox[i][2], &endPtr); 
+                        if ( endPtr != (char *) valTextBox[i][2] ) {
+                            p->tangents.x = value;
+                        }
+                    }
+                }
+                // Right tan
+                if(GuiTextBox((Rectangle){contentRect.x+contentRect.width/2, contentRect.y+contentRect.height+scrollOffset.y, contentRect.width/2, 1.5*fontSize}, valTextBox[i][3], 20, editValueBox[i][3])){
+                    editValueBox[i][3] = !editValueBox[i][3];
+                    // input ended
+                    if(!editValueBox[i][3]){
+                        // Try to convert text to float and assign it to the point
+                        char * endPtr;
+                        double value = strtod( (char *) valTextBox[i][3], &endPtr); 
+                        if ( endPtr != (char *) valTextBox[i][3] ) {
+                            p->tangents.y = value;
+                        }
+                    }
+                }
+                contentRect.height += 1.5*fontSize + margin;
+
+            }
+            contentRect.height += margin;
+            EndScissorMode();
+
+            // Settings panel shadow
+            DrawRectangleGradientH(settingsRect.x-12, 0, 12, settingsRect.height, BLANK, (Color){0,0,0,100});
+
+            // Slider
+            if(moveSlider){
+                DrawRectangle(sliderRect.x, sliderRect.y, sliderRect.width, sliderRect.height, GetColor(GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED)));
+            }else if(CheckCollisionPointRec(GetMousePosition(), sliderRect)){
+                DrawRectangle(sliderRect.x, sliderRect.y, sliderRect.width, sliderRect.height, GetColor(GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED)));
+            }
+
+            // Draw Time controls
+            //----------------------------------------------------------------------------------
+            Rectangle timeLineRect = (Rectangle) {0, screenHeight-4*fontSize, settingsRect.x, 4*fontSize};
+            GuiPanel((Rectangle) { timeLineRect.x, timeLineRect.y, timeLineRect.width, 2*fontSize}, NULL);
+            GuiLabel((Rectangle) { timeLineRect.x, timeLineRect.y, timeLineRect.width, 2*fontSize}, TextFormat("Normalized Time: %.3f", time / animationTime));
+            if(GuiButton((Rectangle) { timeLineRect.x+timeLineRect.width/2-2*fontSize-margin/4, timeLineRect.y, 2*fontSize, 2*fontSize}, GuiIconText(playAnimation ? ICON_PLAYER_PAUSE : ICON_PLAYER_PLAY, ""))){
+                playAnimation = !playAnimation;
+            }
+            if(GuiButton((Rectangle) { timeLineRect.x+timeLineRect.width/2+margin/4, timeLineRect.y, 2*fontSize, 2*fontSize}, GuiIconText(ICON_PLAYER_STOP, ""))){
+                playAnimation = false;
+                time = 0;
+            }
+            time = animationTime * GuiSlider((Rectangle){timeLineRect.x, timeLineRect.y+2*fontSize, timeLineRect.width, timeLineRect.height-2*fontSize}, NULL, NULL, time / animationTime, 0, 1);
+
+            // Time panel shadow
+            DrawRectangleGradientV(timeLineRect.x, timeLineRect.y-12, timeLineRect.width, 12, BLANK, (Color){0,0,0,100});
+
+            
+        EndDrawing();
+        //----------------------------------------------------------------------------------
+    }
+
+    CloseWindow();              // Close window and OpenGL context
+    //--------------------------------------------------------------------------------------
+
+    return 0;
+}
+
+void LoadDefaults(GuiCurveEditState curves[]){
+    // X pos
+    curves[0].start = 28;
+    curves[0].end = 506;
+    curves[0].numPoints = 4;
+    curves[0].selectedIndex = 0;
+    curves[0].editLeftTangent = false;
+    curves[0].editRightTangent = false;
+    curves[0].points[0].position =(Vector2) {0.000000, 0.000000}; curves[0].points[0].tangents = (Vector2) {0.000000, 1.515101};  curves[0].points[0].leftLinear =   1;curves[0].points[0].rightLinear =  1;
+    curves[0].points[1].position =(Vector2) {0.422414, 0.640000}; curves[0].points[1].tangents = (Vector2) {-2.824348, -4.494999};curves[0].points[1].leftLinear =   0;curves[0].points[1].rightLinear =  0;
+    curves[0].points[2].position =(Vector2) {0.732759, 0.210000}; curves[0].points[2].tangents = (Vector2) {0.000000, 2.956133};  curves[0].points[2].leftLinear =   0;curves[0].points[2].rightLinear =  1;
+    curves[0].points[3].position =(Vector2) {1.000000, 1.000000}; curves[0].points[3].tangents = (Vector2) {2.956133, 0.000000};  curves[0].points[3].leftLinear =   1;curves[0].points[3].rightLinear =  1;
+
+    // Y pos
+    curves[1].start = 405;
+    curves[1].end = 135;
+    curves[1].numPoints = 7;
+    curves[1].selectedIndex = 0;
+    curves[1].editLeftTangent = false;
+    curves[1].editRightTangent = false;
+    curves[1].points[0].position = (Vector2) {0.000000, 1.000000};curves[1].points[0].tangents = (Vector2) { 0.000000  , 0.000000};curves[1].points[0].leftLinear =  0;curves[1].points[0].rightLinear =  0;
+    curves[1].points[1].position = (Vector2) {0.140000, 0.000000};curves[1].points[1].tangents = (Vector2) {-10.000000 ,10.000000};curves[1].points[1].leftLinear =  0;curves[1].points[1].rightLinear =  0;
+    curves[1].points[2].position = (Vector2) {0.450000, 0.000000};curves[1].points[2].tangents = (Vector2) {-10.000000 ,10.000000};curves[1].points[2].leftLinear =  0;curves[1].points[2].rightLinear =  0;
+    curves[1].points[3].position = (Vector2) {0.670000, 0.000000};curves[1].points[3].tangents = (Vector2) {-10.000000 ,10.000000};curves[1].points[3].leftLinear =  0;curves[1].points[3].rightLinear =  0;
+    curves[1].points[4].position = (Vector2) {0.830000, 0.000000};curves[1].points[4].tangents = (Vector2) {-10.000000 ,10.000000};curves[1].points[4].leftLinear =  0;curves[1].points[4].rightLinear =  0;
+    curves[1].points[5].position = (Vector2) {0.940000, 0.000000};curves[1].points[5].tangents = (Vector2) {-10.000000 ,10.000000};curves[1].points[5].leftLinear =  0;curves[1].points[5].rightLinear =  0;
+    curves[1].points[6].position = (Vector2) {1.000000, 0.000000};curves[1].points[6].tangents = (Vector2) {-10.000000 , 0.000000};curves[1].points[6].leftLinear =  0;curves[1].points[6].rightLinear =  0;
+
+    // X size
+    curves[2].start = 1;
+    curves[2].end = 64;
+    curves[2].numPoints = 16;
+    curves[2].selectedIndex = 0;
+    curves[2].editLeftTangent = false;
+    curves[2].editRightTangent = false;
+    curves[2].points[0].position =  (Vector2) {0.000000, 0.492063}; curves[2].points[0].tangents =  (Vector2) {0,0}; curves[2].points[0].leftLinear =  0; curves[2].points[0].rightLinear =  0;
+    curves[2].points[1].position =  (Vector2) {0.130000, 0.492063}; curves[2].points[1].tangents =  (Vector2) {0,0}; curves[2].points[1].leftLinear =  0; curves[2].points[1].rightLinear =  0;
+    curves[2].points[2].position =  (Vector2) {0.140000, 0.746032}; curves[2].points[2].tangents =  (Vector2) {0,0}; curves[2].points[2].leftLinear =  0; curves[2].points[2].rightLinear =  0;
+    curves[2].points[3].position =  (Vector2) {0.150000, 0.492063}; curves[2].points[3].tangents =  (Vector2) {0,0}; curves[2].points[3].leftLinear =  0; curves[2].points[3].rightLinear =  0;
+    curves[2].points[4].position =  (Vector2) {0.440000, 0.490000}; curves[2].points[4].tangents =  (Vector2) {0,0}; curves[2].points[4].leftLinear =  0; curves[2].points[4].rightLinear =  0;
+    curves[2].points[5].position =  (Vector2) {0.450000, 0.682540}; curves[2].points[5].tangents =  (Vector2) {0,0}; curves[2].points[5].leftLinear =  0; curves[2].points[5].rightLinear =  0;
+    curves[2].points[6].position =  (Vector2) {0.460000, 0.480000}; curves[2].points[6].tangents =  (Vector2) {0,0}; curves[2].points[6].leftLinear =  0; curves[2].points[6].rightLinear =  0;
+    curves[2].points[7].position =  (Vector2) {0.660000, 0.492063}; curves[2].points[7].tangents =  (Vector2) {0,0}; curves[2].points[7].leftLinear =  0; curves[2].points[7].rightLinear =  0;
+    curves[2].points[8].position =  (Vector2) {0.670000, 0.619048}; curves[2].points[8].tangents =  (Vector2) {0,0}; curves[2].points[8].leftLinear =  0; curves[2].points[8].rightLinear =  0;
+    curves[2].points[9].position =  (Vector2) {0.680000, 0.492063}; curves[2].points[9].tangents =  (Vector2) {0,0}; curves[2].points[9].leftLinear =  0; curves[2].points[9].rightLinear =  0;
+    curves[2].points[10].position = (Vector2) {0.820000, 0.492063}; curves[2].points[10].tangents = (Vector2) {0,0}; curves[2].points[10].leftLinear = 0; curves[2].points[10].rightLinear = 0;
+    curves[2].points[11].position = (Vector2) {0.830000, 0.619048}; curves[2].points[11].tangents = (Vector2) {0,0}; curves[2].points[11].leftLinear = 0; curves[2].points[11].rightLinear = 0;
+    curves[2].points[12].position = (Vector2) {0.840000, 0.492063}; curves[2].points[12].tangents = (Vector2) {0,0}; curves[2].points[12].leftLinear = 0; curves[2].points[12].rightLinear = 0;
+    curves[2].points[13].position = (Vector2) {0.930000, 0.492063}; curves[2].points[13].tangents = (Vector2) {0,0}; curves[2].points[13].leftLinear = 0; curves[2].points[13].rightLinear = 0;
+    curves[2].points[14].position = (Vector2) {0.940000, 0.619048}; curves[2].points[14].tangents = (Vector2) {0,0}; curves[2].points[14].leftLinear = 0; curves[2].points[14].rightLinear = 0;
+    curves[2].points[15].position = (Vector2) {0.950000, 0.492063}; curves[2].points[15].tangents = (Vector2) {0,0}; curves[2].points[15].leftLinear = 0; curves[2].points[15].rightLinear = 0;
+
+    // Y Size
+    curves[3].start = 1;
+    curves[3].end = 64;
+    curves[3].numPoints = 16;
+    curves[3].selectedIndex = 0;
+    curves[3].editLeftTangent = false;
+    curves[3].editRightTangent = false;
+    curves[3].points[0].position =  (Vector2) {0.000000, 0.492063};curves[3].points[0].tangents =  (Vector2) {0,0};curves[3].points[0].leftLinear =  0;curves[3].points[0].rightLinear =  0;
+    curves[3].points[1].position =  (Vector2) {0.130000, 0.492063};curves[3].points[1].tangents =  (Vector2) {0,0};curves[3].points[1].leftLinear =  0;curves[3].points[1].rightLinear =  0;
+    curves[3].points[2].position =  (Vector2) {0.140000, 0.238095};curves[3].points[2].tangents =  (Vector2) {0,0};curves[3].points[2].leftLinear =  0;curves[3].points[2].rightLinear =  0;
+    curves[3].points[3].position =  (Vector2) {0.150000, 0.492063};curves[3].points[3].tangents =  (Vector2) {0,0};curves[3].points[3].leftLinear =  0;curves[3].points[3].rightLinear =  0;
+    curves[3].points[4].position =  (Vector2) {0.440000, 0.492063};curves[3].points[4].tangents =  (Vector2) {0,0};curves[3].points[4].leftLinear =  0;curves[3].points[4].rightLinear =  0;
+    curves[3].points[5].position =  (Vector2) {0.450000, 0.301587};curves[3].points[5].tangents =  (Vector2) {0,0};curves[3].points[5].leftLinear =  0;curves[3].points[5].rightLinear =  0;
+    curves[3].points[6].position =  (Vector2) {0.460000, 0.492063};curves[3].points[6].tangents =  (Vector2) {0,0};curves[3].points[6].leftLinear =  0;curves[3].points[6].rightLinear =  0;
+    curves[3].points[7].position =  (Vector2) {0.660000, 0.492063};curves[3].points[7].tangents =  (Vector2) {0,0};curves[3].points[7].leftLinear =  0;curves[3].points[7].rightLinear =  0;
+    curves[3].points[8].position =  (Vector2) {0.670000, 0.365079};curves[3].points[8].tangents =  (Vector2) {0,0};curves[3].points[8].leftLinear =  0;curves[3].points[8].rightLinear =  0;
+    curves[3].points[9].position =  (Vector2) {0.680000, 0.492063};curves[3].points[9].tangents =  (Vector2) {0,0};curves[3].points[9].leftLinear =  0;curves[3].points[9].rightLinear =  0;
+    curves[3].points[10].position = (Vector2) {0.820000, 0.492063};curves[3].points[10].tangents = (Vector2) {0,0};curves[3].points[10].leftLinear = 0;curves[3].points[10].rightLinear = 0;
+    curves[3].points[11].position = (Vector2) {0.830000, 0.365079};curves[3].points[11].tangents = (Vector2) {0,0};curves[3].points[11].leftLinear = 0;curves[3].points[11].rightLinear = 0;
+    curves[3].points[12].position = (Vector2) {0.840000, 0.492063};curves[3].points[12].tangents = (Vector2) {0,0};curves[3].points[12].leftLinear = 0;curves[3].points[12].rightLinear = 0;
+    curves[3].points[13].position = (Vector2) {0.930000, 0.492063};curves[3].points[13].tangents = (Vector2) {0,0};curves[3].points[13].leftLinear = 0;curves[3].points[13].rightLinear = 0;
+    curves[3].points[14].position = (Vector2) {0.940000, 0.365079};curves[3].points[14].tangents = (Vector2) {0,0};curves[3].points[14].leftLinear = 0;curves[3].points[14].rightLinear = 0;
+    curves[3].points[15].position = (Vector2) {0.950000, 0.507937};curves[3].points[15].tangents = (Vector2) {0,0};curves[3].points[15].leftLinear = 0;curves[3].points[15].rightLinear = 0;
+
+    // Rotation
+
+    curves[4].start = -360;
+    curves[4].end = 360;
+    curves[4].numPoints = 9;
+    curves[4].selectedIndex = 0;
+    curves[4].editLeftTangent = false;
+    curves[4].editRightTangent = false;
+    curves[4].points[0].position =  (Vector2) {0.140000, 0.500000};curves[4].points[0].tangents =  (Vector2) {0,0};curves[4].points[0].leftLinear =  0;curves[4].points[0].rightLinear =  0;
+    curves[4].points[1].position =  (Vector2) {0.450000, 0.500000};curves[4].points[1].tangents =  (Vector2) {0,0};curves[4].points[1].leftLinear =  0;curves[4].points[1].rightLinear =  0;
+    curves[4].points[2].position =  (Vector2) {0.670000, 0.500000};curves[4].points[2].tangents =  (Vector2) {0,0};curves[4].points[2].leftLinear =  0;curves[4].points[2].rightLinear =  0;
+    curves[4].points[3].position =  (Vector2) {0.830000, 0.500000};curves[4].points[3].tangents =  (Vector2) {0,0};curves[4].points[3].leftLinear =  0;curves[4].points[3].rightLinear =  0;
+    curves[4].points[4].position =  (Vector2) {0.940000, 0.500000};curves[4].points[4].tangents =  (Vector2) {0,0};curves[4].points[4].leftLinear =  0;curves[4].points[4].rightLinear =  0;
+    curves[4].points[5].position =  (Vector2) {1.000000, 0.500000};curves[4].points[5].tangents =  (Vector2) {0,0};curves[4].points[5].leftLinear =  0;curves[4].points[5].rightLinear =  0;
+    curves[4].points[6].position =  (Vector2) {0.000000, 0.472222};curves[4].points[6].tangents =  (Vector2) {0,0};curves[4].points[6].leftLinear =  0;curves[4].points[6].rightLinear =  0;
+    curves[4].points[7].position =  (Vector2) {0.302752, 0.527778};curves[4].points[7].tangents =  (Vector2) {0,0};curves[4].points[7].leftLinear =  0;curves[4].points[7].rightLinear =  0;
+    curves[4].points[8].position =  (Vector2) {0.577982, 0.472222};curves[4].points[8].tangents =  (Vector2) {0,0};curves[4].points[8].leftLinear =  0;curves[4].points[8].rightLinear =  0;
+}
+
+

+ 517 - 0
examples/animation_curve/gui_curve_edit.h

@@ -0,0 +1,517 @@
+/*******************************************************************************************
+*
+*   CurveEdit v1.0 - A cubic Hermite editor for making animation curves
+*
+*   MODULE USAGE:
+*       #define GUI_CURVE_EDIT_IMPLEMENTATION
+*       #include "gui_curve_edit.h"
+*
+*       INIT: GuiCurveEditState state = InitCurveEdit();
+*       EVALUATE: float y = state.start + (state.end-state.start) * EvalGuiCurve(&state, t); // 0 <= t <= 1
+*       DRAW: BeginScissorMode(bounds.x,bounds.y,bounds.width,bounds.height); 
+*               GuiCurveEdit(&state, bounds, pointSize);
+*             EndScissorMode(); 
+*
+*   NOTE: See 'Module Structures Declaration' section for more informations.
+*
+*   NOTE: This module uses functions of the stdlib:
+*       - qsort
+*
+*   NOTE: Built-in interactions:
+*       - Left click to move/add point or move tangents
+*       - While moving a tangent, hold (left/right) SHIFT to disable tangent symetry 
+*       - Right click to remove a point
+*
+*
+*   LICENSE: zlib/libpng
+*
+*   Copyright (c) 2023 Pierre Jaffuer (@smallcluster)
+*
+*   This software is provided "as-is", without any express or implied warranty. In no event
+*   will the authors be held liable for any damages arising from the use of this software.
+*
+*   Permission is granted to anyone to use this software for any purpose, including commercial
+*   applications, and to alter it and redistribute it freely, subject to the following restrictions:
+*
+*     1. The origin of this software must not be misrepresented; you must not claim that you
+*     wrote the original software. If you use this software in a product, an acknowledgment
+*     in the product documentation would be appreciated but is not required.
+*
+*     2. Altered source versions must be plainly marked as such, and must not be misrepresented
+*     as being the original software.
+*
+*     3. This notice may not be removed or altered from any source distribution.
+*
+**********************************************************************************************/
+
+#include "raylib.h"
+
+#ifndef GUI_CURVE_EDIT_H
+#define GUI_CURVE_EDIT_H
+
+
+#ifndef GUI_CURVE_EDIT_MAX_POINTS
+    #define GUI_CURVE_EDIT_MAX_POINTS 30
+#endif 
+
+//----------------------------------------------------------------------------------
+// Module Structures Declaration
+//----------------------------------------------------------------------------------
+
+typedef struct {
+    Vector2 position = (Vector2) {0,0}; // In normalized space [0.f, 1.f]
+    Vector2 tangents = (Vector2) {0,0}; // The derivatives (left/right) of the 1D curve
+    // Let the curve editor calculate tangents to linearize part of the curve
+    bool leftLinear = false;
+    bool rightLinear = false;
+} GuiCurveEditPoint;
+
+typedef struct {
+    float start = 0; // Value at y = 0
+    float end = 1; // Value at y = 1
+    // Always valid (unless you manualy change state's point array)
+    int selectedIndex = -1; // -1 before Init
+    // Unsorted array with at least one point (constant curve)
+    GuiCurveEditPoint points[GUI_CURVE_EDIT_MAX_POINTS];
+    int numPoints = 0;
+
+    // private part
+    bool editLeftTangent = false;
+    bool editRightTangent = false;
+    Vector2 mouseOffset = (Vector2) {0,0};
+} GuiCurveEditState;
+
+
+#ifdef __cplusplus
+extern "C" {            // Prevents name mangling of functions
+#endif
+
+//----------------------------------------------------------------------------------
+// Module Functions Declaration
+//----------------------------------------------------------------------------------
+
+// Initialize state 
+GuiCurveEditState InitGuiCurveEdit();
+// Draw and update curve control
+void GuiCurveEdit(GuiCurveEditState *state, Rectangle bounds);
+
+// 1D Interpolation
+// Returns the normalized y value of the curve at x = t
+// t must be normalized [0.f, 1.f]
+float EvalGuiCurve(GuiCurveEditState *state, float t);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // GUI_CURVE_EDIT_H
+
+/***********************************************************************************
+*
+*   GUI_CURVE_EDIT IMPLEMENTATION
+*
+************************************************************************************/
+#if defined(GUI_CURVE_EDIT_IMPLEMENTATION)
+
+#include "../../src/raygui.h" // Change this to fit your project
+
+#include "stdlib.h" // Required for qsort
+
+//----------------------------------------------------------------------------------
+// Module Functions Definition
+//----------------------------------------------------------------------------------
+
+GuiCurveEditState InitGuiCurveEdit()
+{
+    GuiCurveEditState state;
+
+    state.start = 0;
+    state.end = 1;
+    state.selectedIndex = 0;
+    state.editLeftTangent = false;
+    state.editRightTangent = false;
+    state.mouseOffset = (Vector2) {0.f,0.f};
+
+    // At least one point (AVG by default)
+    state.numPoints = 1;
+    state.points[0].position = (Vector2) {0.5f,0.5f};
+    state.points[0].tangents = (Vector2) {0.f,0.f};
+    state.points[0].leftLinear = false;
+    state.points[0].rightLinear = false;
+
+    return state;
+}
+
+int CompareGuiCurveEditPointPtr (const void * a, const void * b)
+{
+  float fa = (*(GuiCurveEditPoint**)a)->position.x;
+  float fb = (*(GuiCurveEditPoint**)b)->position.x;
+  return (fa > fb) - (fa < fb);
+}
+
+float EvalGuiCurve(GuiCurveEditState *state, float t){
+
+    // Sort points
+    GuiCurveEditPoint* sortedPoints[GUI_CURVE_EDIT_MAX_POINTS];
+    for(int i=0; i < state->numPoints; i++){
+        sortedPoints[i] = &state->points[i];
+    }
+    qsort(sortedPoints, state->numPoints, sizeof(GuiCurveEditPoint*), CompareGuiCurveEditPointPtr);
+
+    if(state->numPoints == 0)
+        return state->start;
+
+    // Constants part on edges
+    if(t <= sortedPoints[0]->position.x)
+        return state->start + (state->end-state->start) * sortedPoints[0]->position.y;
+    if(t >= sortedPoints[state->numPoints-1]->position.x)
+        return state->start + (state->end-state->start) * sortedPoints[state->numPoints-1]->position.y;
+
+    // Find curve portion
+    for(int i=0; i < state->numPoints-1; i++){
+        const GuiCurveEditPoint* p1 = sortedPoints[i];
+        const GuiCurveEditPoint* p2 = sortedPoints[i+1];
+        // Skip this range
+        if(!(t >= p1->position.x && t < p2->position.x) || p1->position.x == p2->position.x)
+            continue;
+        float scale = (p2->position.x-p1->position.x);
+        float T = (t-p1->position.x)/scale;
+        float startTangent = scale * p1->tangents.y;
+        float endTangent = scale * p2->tangents.x;
+        float T2 = T*T;
+        float T3 = T*T*T;
+        return state->start + (state->end-state->start) * ((2*T3-3*T2+1)*p1->position.y+(T3-2*T2+T)*startTangent+(3*T2-2*T3)*p2->position.y+(T3-T2)*endTangent);
+    }
+
+    return state->start;
+}
+
+void GuiCurveEdit(GuiCurveEditState *state, Rectangle bounds){
+
+    //----------------------------------------------------------------------------------
+    // CONST
+    //----------------------------------------------------------------------------------
+    const float pointSize = 10;
+    const float fontSize = GuiGetStyle(DEFAULT, TEXT_SIZE);
+    const float handleLength = pointSize*2.5;
+    const float handleSize = pointSize/1.5f;
+
+    const Rectangle innerBounds = (Rectangle){bounds.x+fontSize, bounds.y+fontSize, bounds.width-2*fontSize, bounds.height-2*fontSize};
+    const Vector2 mouse = GetMousePosition();
+    const Vector2 mouseLocal = (Vector2) {(mouse.x-innerBounds.x)/innerBounds.width, (innerBounds.y+innerBounds.height-mouse.y)/innerBounds.height};
+
+    //----------------------------------------------------------------------------------
+    // UPDATE STATE
+    //----------------------------------------------------------------------------------
+
+    // Find first point under mouse (-1 if not found)
+    int hoveredPointIndex = -1;
+    for(int i=0; i < state->numPoints; i++){
+        const GuiCurveEditPoint* p = &state->points[i];
+        const Vector2 screenPos = (Vector2){p->position.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-p->position.y*innerBounds.height};
+        const Rectangle pointRect = (Rectangle) {screenPos.x-pointSize/2.f, screenPos.y-pointSize/2.f, pointSize, pointSize};
+        if(CheckCollisionPointRec(mouse, pointRect)){
+            hoveredPointIndex = i;
+            break;
+        }
+    }
+
+    // Unselect tangents
+    if(IsMouseButtonReleased(MOUSE_BUTTON_LEFT)){
+        state->editLeftTangent = false;
+        state->editRightTangent = false;
+    }
+
+    // Select a tangent if possible
+    if(IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && state->selectedIndex != -1 && CheckCollisionPointRec(mouse, bounds)){
+        const GuiCurveEditPoint* p = &state->points[state->selectedIndex];
+        const Vector2 screenPos = (Vector2){p->position.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-p->position.y*innerBounds.height};
+
+        // Left control
+        Vector2 target = (Vector2) {(p->position.x-1)*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-(p->position.y-p->tangents.x)*innerBounds.height};
+        Vector2 dir = (Vector2) {target.x-screenPos.x, target.y-screenPos.y};
+        float d = sqrt(dir.x*dir.x+dir.y*dir.y);
+        Vector2 control = (Vector2) {screenPos.x+dir.x/d*handleLength, screenPos.y+dir.y/d*handleLength};
+        Rectangle controlRect = (Rectangle) {control.x-handleSize/2.f, control.y-handleSize/2.f, handleSize, handleSize};
+
+        // Edit left tangent
+        if(CheckCollisionPointRec(mouse, controlRect)){
+            state->editLeftTangent = true;
+        }
+
+        // Right control
+        target = (Vector2) {(p->position.x+1)*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-(p->position.y+p->tangents.y)*innerBounds.height};
+        dir = (Vector2) {target.x-screenPos.x, target.y-screenPos.y};
+        d = sqrt(dir.x*dir.x+dir.y*dir.y);
+        control = (Vector2) {screenPos.x+dir.x/d*handleLength, screenPos.y+dir.y/d*handleLength};
+        controlRect = (Rectangle) {control.x-handleSize/2.f, control.y-handleSize/2.f, handleSize, handleSize};
+        // Edit right tangent
+        if(CheckCollisionPointRec(mouse, controlRect)){
+            state->editRightTangent = true;
+        }
+    }
+
+    
+    // Move tangents
+    if(IsMouseButtonDown(MOUSE_BUTTON_LEFT) && state->editRightTangent){
+        // editRightTangent == true implies selectedIndex != -1
+        GuiCurveEditPoint* p = &state->points[state->selectedIndex]; 
+        const Vector2 screenPos = (Vector2){p->position.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-p->position.y*innerBounds.height};
+        const Vector2 dir = (Vector2){mouseLocal.x-p->position.x, mouseLocal.y-p->position.y};
+        // Calculate right tangent slope 
+        p->tangents.y = dir.x < 0.001f ? dir.y/0.001f : dir.y/dir.x;
+        p->rightLinear = false; // Stop right linearization update
+
+        // Tangents are symetric by default unless SHIFT is pressed
+        if(!(IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT))){
+            p->tangents.x = p->tangents.y;
+            p->leftLinear = false; // Stop left linearization update
+        }
+
+    } else if(IsMouseButtonDown(MOUSE_BUTTON_LEFT) && state->editLeftTangent){
+        // editLeftTangent == true implies selectedIndex != -1
+        GuiCurveEditPoint* p = &state->points[state->selectedIndex];
+        const Vector2 screenPos = (Vector2){p->position.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-p->position.y*innerBounds.height};
+        const Vector2 dir = (Vector2){mouseLocal.x-p->position.x, mouseLocal.y-p->position.y};
+        // Calculate left tangent slope 
+        p->tangents.x = dir.x > -0.001f ? dir.y/(-0.001f) : dir.y/dir.x;
+        p->leftLinear = false; // Stop left linearization update
+
+        // Tangents are symetric by default unless SHIFT is pressed
+        if(!(IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT))){
+            p->tangents.y = p->tangents.x;
+            p->rightLinear = false; // Stop right linearization update
+        }
+    } 
+    // Select a point
+    else if(IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && hoveredPointIndex != -1 CheckCollisionPointRec(mouse, bounds)){
+        state->selectedIndex = hoveredPointIndex;
+        const GuiCurveEditPoint* p = &state->points[state->selectedIndex];
+        const Vector2 screenPos = (Vector2){p->position.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-p->position.y*innerBounds.height};
+        state->mouseOffset = (Vector2) {p->position.x - mouseLocal.x, p->position.y - mouseLocal.y};
+    }
+    // Remove a point (check against bounds)
+    else if(IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && hoveredPointIndex != -1 && CheckCollisionPointRec(mouse, bounds) && state->numPoints > 1){
+        // Deselect everything
+        state->selectedIndex = 0; // select first point by default
+        state->editLeftTangent = false;
+        state->editRightTangent = false;
+
+        // Remove point
+        state->numPoints -= 1;
+        for(int i=hoveredPointIndex; i < state->numPoints; i++ ){
+            state->points[i] = state->points[i+1];
+        }
+        
+    // Add a point (check against innerBounds)
+    } else if(IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && CheckCollisionPointRec(mouse, innerBounds) && state->numPoints < GUI_CURVE_EDIT_MAX_POINTS){
+        state->editLeftTangent = false;
+        state->editRightTangent = false;
+
+        // Create new point
+        GuiCurveEditPoint p;
+        p.tangents = (Vector2) {0.f, 0.f};
+        p.position = mouseLocal;
+        p.leftLinear = false;
+        p.rightLinear = false;
+        // Append point
+        state->points[state->numPoints] = p;
+        state->selectedIndex = state->numPoints; // select new point
+        state->numPoints += 1;
+        // Point is add on mouse pos
+        state->mouseOffset = (Vector2) {0,0};
+
+    // Move selected point
+    } else if(state->selectedIndex != -1 && IsMouseButtonDown(MOUSE_BUTTON_LEFT) && CheckCollisionPointRec(mouse, bounds) ){
+        GuiCurveEditPoint* p = &state->points[state->selectedIndex];
+
+        // use mouse offset on click to prevent point teleporting to mouse
+        const Vector2 newLocalPos = (Vector2){mouseLocal.x + state->mouseOffset.x, mouseLocal.y + state->mouseOffset.y};
+
+        // Clamp to innerbounds
+        p->position.x = newLocalPos.x < 0 ? 0 : newLocalPos.x > 1 ? 1 : newLocalPos.x;
+        p->position.y = newLocalPos.y < 0 ? 0 : newLocalPos.y > 1 ? 1 : newLocalPos.y;
+    }
+
+    // Sort points
+    GuiCurveEditPoint* sortedPoints[GUI_CURVE_EDIT_MAX_POINTS];
+    for(int i=0; i < state->numPoints; i++){
+        sortedPoints[i] = &state->points[i];
+    }
+    qsort(sortedPoints, state->numPoints, sizeof(GuiCurveEditPoint*), CompareGuiCurveEditPointPtr);
+
+
+    // Update linear tangents
+    for(int i=0; i < state->numPoints; i++){
+        GuiCurveEditPoint* p = sortedPoints[i];
+        // Left tangent
+        if(i > 0 && p->leftLinear){
+            const GuiCurveEditPoint* p2 = sortedPoints[i-1];
+            Vector2 dir = (Vector2) {p2->position.x-p->position.x, p2->position.y-p->position.y};
+            p->tangents.x = dir.x == 0 ? 0 : dir.y/dir.x;
+        }
+        // Right tangent
+        if(i < state->numPoints-1 && p->rightLinear){
+            const GuiCurveEditPoint* p2 = sortedPoints[i+1];
+            Vector2 dir = (Vector2) {p2->position.x-p->position.x, p2->position.y-p->position.y};
+            p->tangents.y = dir.x == 0 ? 0 : dir.y/dir.x;
+        }
+    }
+
+    //----------------------------------------------------------------------------------
+    // DRAWING
+    //----------------------------------------------------------------------------------
+
+    // Draw bg
+    DrawRectangle(bounds.x, bounds.y, bounds.width, bounds.height, GetColor(GuiGetStyle(DEFAULT, BACKGROUND_COLOR)));
+
+    // Draw grid
+    // H lines
+    const Color lineColor = GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL));
+    DrawLine(bounds.x, innerBounds.y, bounds.x+bounds.width, innerBounds.y, lineColor); // end
+    DrawLine(bounds.x, innerBounds.y+innerBounds.height/2, bounds.x+bounds.width, innerBounds.y+innerBounds.height/2, lineColor); // avg
+    DrawLine(bounds.x, innerBounds.y+innerBounds.height, bounds.x+bounds.width, innerBounds.y+innerBounds.height, lineColor); // start
+
+    // V lines
+    DrawLine(innerBounds.x, bounds.y, innerBounds.x, bounds.y+bounds.height, lineColor); // 0
+    DrawLine(innerBounds.x+innerBounds.width/4, bounds.y, innerBounds.x+innerBounds.width/4, bounds.y+bounds.height, lineColor); // 0.25
+    DrawLine(innerBounds.x+innerBounds.width/2, bounds.y, innerBounds.x+innerBounds.width/2, bounds.y+bounds.height, lineColor); // 0.5
+    DrawLine(innerBounds.x+3*innerBounds.width/4, bounds.y, innerBounds.x+3*innerBounds.width/4, bounds.y+bounds.height, lineColor); // 0.75
+    DrawLine(innerBounds.x+innerBounds.width, bounds.y, innerBounds.x+innerBounds.width, bounds.y+bounds.height, lineColor); // 1
+
+    Font font = GuiGetFont();
+    // V labels
+    DrawTextEx(font, "0",    (Vector2) {innerBounds.x, bounds.y+bounds.height-fontSize}, fontSize,GuiGetStyle(DEFAULT, TEXT_SPACING), lineColor);
+    DrawTextEx(font, "0.25", (Vector2) {innerBounds.x+innerBounds.width/4.f, bounds.y+bounds.height-fontSize}, fontSize,GuiGetStyle(DEFAULT, TEXT_SPACING), lineColor);
+    DrawTextEx(font, "0.5",  (Vector2) {innerBounds.x+innerBounds.width/2.f, bounds.y+bounds.height-fontSize}, fontSize,GuiGetStyle(DEFAULT, TEXT_SPACING), lineColor);
+    DrawTextEx(font, "0.75", (Vector2) {innerBounds.x+3.f*innerBounds.width/4.f, bounds.y+bounds.height-fontSize}, fontSize,GuiGetStyle(DEFAULT, TEXT_SPACING), lineColor);
+    DrawTextEx(font, "1",    (Vector2) {innerBounds.x+innerBounds.width, bounds.y+bounds.height-fontSize}, fontSize,GuiGetStyle(DEFAULT, TEXT_SPACING), lineColor);
+
+    // H labels
+    DrawTextEx(font, TextFormat("%.2f", state->start), (Vector2) {innerBounds.x, innerBounds.y-fontSize+innerBounds.height}, fontSize, GuiGetStyle(DEFAULT, TEXT_SPACING), lineColor);
+    DrawTextEx(font, TextFormat("%.2f", state->start + (state->end-state->start)/2.f), (Vector2) {innerBounds.x, innerBounds.y-fontSize+innerBounds.height/2.f}, fontSize, GuiGetStyle(DEFAULT, TEXT_SPACING), lineColor);
+    DrawTextEx(font, TextFormat("%.2f", state->end), (Vector2) {innerBounds.x, innerBounds.y}, fontSize, GuiGetStyle(DEFAULT, TEXT_SPACING), lineColor);
+
+    // Draw contours
+    if(CheckCollisionPointRec(mouse, bounds))
+        DrawRectangleLines(bounds.x, bounds.y, bounds.width, bounds.height, GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_FOCUSED)));
+    else
+        DrawRectangleLines(bounds.x, bounds.y, bounds.width, bounds.height, GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL)));
+
+    // Draw points
+    for(int i=0; i < state->numPoints; i++){
+
+        const GuiCurveEditPoint* p = sortedPoints[i];
+
+        const Vector2 screenPos = (Vector2){p->position.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-p->position.y*innerBounds.height};
+        const Rectangle pointRect = (Rectangle) {screenPos.x-pointSize/2.f, screenPos.y-pointSize/2.f, pointSize, pointSize};
+
+        Color pointColor;
+        Color pointBorderColor;
+
+        // Draw point
+        if(&state->points[state->selectedIndex] == p){
+
+            // Draw left handle
+            if(i > 0){
+                const Vector2 target = (Vector2) {(p->position.x-1)*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-(p->position.y-p->tangents.x)*innerBounds.height};
+                const Vector2 dir = (Vector2) {target.x-screenPos.x, target.y-screenPos.y};
+                const float d = sqrt(dir.x*dir.x+dir.y*dir.y);
+                const Vector2 control = (Vector2) {screenPos.x+dir.x/d*handleLength, screenPos.y+dir.y/d*handleLength};
+                const Rectangle controlRect = (Rectangle) {control.x-handleSize/2.f, control.y-handleSize/2.f, handleSize, handleSize};
+
+                Color controlColor;
+                Color controlBorderColor;
+                if(state->editLeftTangent){
+                    controlColor = GetColor(GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED));
+                    controlBorderColor =  GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL));
+                } else if(CheckCollisionPointRec(mouse, controlRect)){
+                    controlColor = GetColor(GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED));
+                    controlBorderColor = GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL));
+                }else{
+                    controlColor = GetColor(GuiGetStyle(BUTTON, BASE_COLOR_NORMAL));
+                    controlBorderColor = GetColor(GuiGetStyle(BUTTON, BORDER_COLOR_NORMAL));
+                }
+                DrawLine(screenPos.x,screenPos.y, control.x, control.y, controlColor);
+                DrawRectangle(controlRect.x, controlRect.y, controlRect.width, controlRect.height, controlColor);
+                DrawRectangleLines(controlRect.x, controlRect.y, controlRect.width, controlRect.height, controlColor);
+            }
+            // Draw right handle
+            if(i < state->numPoints-1){
+                const Vector2 target = (Vector2) {(p->position.x+1)*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-(p->position.y+p->tangents.y)*innerBounds.height};
+                const Vector2 dir = (Vector2) {target.x-screenPos.x, target.y-screenPos.y};
+                const float d = sqrt(dir.x*dir.x+dir.y*dir.y);
+                const Vector2 control = (Vector2) {screenPos.x+dir.x/d*handleLength, screenPos.y+dir.y/d*handleLength};
+                const Rectangle controlRect = (Rectangle) {control.x-handleSize/2.f, control.y-handleSize/2.f, handleSize, handleSize};
+
+                Color controlColor;
+                Color controlBorderColor;
+                if(state->editRightTangent){
+                    controlColor = GetColor(GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED));
+                    controlBorderColor =  GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL));
+                } else if(CheckCollisionPointRec(mouse, controlRect)){
+                    controlColor = GetColor(GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED));
+                    controlBorderColor = GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL));
+                }else{
+                    controlColor = GetColor(GuiGetStyle(BUTTON, BASE_COLOR_NORMAL));
+                    controlBorderColor = GetColor(GuiGetStyle(BUTTON, BORDER_COLOR_NORMAL));
+                }
+                DrawLine(screenPos.x,screenPos.y, control.x, control.y, controlColor);
+                DrawRectangle(controlRect.x, controlRect.y, controlRect.width, controlRect.height, controlColor);
+                DrawRectangleLines(controlRect.x, controlRect.y, controlRect.width, controlRect.height, controlColor);
+            }
+
+            pointColor = GetColor(GuiGetStyle(DEFAULT, BASE_COLOR_PRESSED));
+            pointBorderColor = GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL));
+
+        } else if(&state->points[hoveredPointIndex] == p) {
+            pointColor = GetColor(GuiGetStyle(DEFAULT, BASE_COLOR_FOCUSED));
+            pointBorderColor = GetColor(GuiGetStyle(DEFAULT, BORDER_COLOR_NORMAL));
+        } else {
+            pointColor = GetColor(GuiGetStyle(BUTTON, BASE_COLOR_NORMAL));
+            pointBorderColor = GetColor(GuiGetStyle(BUTTON, BORDER_COLOR_NORMAL));
+        }
+
+        DrawRectangle(pointRect.x, pointRect.y, pointRect.width, pointRect.height, pointColor);
+        DrawRectangleLines(pointRect.x, pointRect.y, pointRect.width, pointRect.height, pointBorderColor);
+
+    }
+
+    // Draw curve
+    Color curveColor = GetColor(GuiGetStyle(LABEL,  TEXT_COLOR_FOCUSED));
+    if(state->numPoints == 1){
+        const GuiCurveEditPoint* p = sortedPoints[0];
+        const Vector2 screenPos = (Vector2){p->position.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-p->position.y*innerBounds.height};
+        DrawLine(innerBounds.x, screenPos.y, innerBounds.x+innerBounds.width, screenPos.y, curveColor);
+    }else {
+        for(int i=0; i < state->numPoints-1; i++){
+            const GuiCurveEditPoint* p1 = sortedPoints[i];
+            const GuiCurveEditPoint* p2 = sortedPoints[i+1];
+            const Vector2 screenPos1 = (Vector2){p1->position.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-p1->position.y*innerBounds.height};
+            const Vector2 screenPos2 = (Vector2){p2->position.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-p2->position.y*innerBounds.height};
+            // Constant on edge
+            if(screenPos1.x > innerBounds.x && i == 0){
+                DrawLine(innerBounds.x, screenPos1.y, screenPos1.x, screenPos1.y, curveColor);
+            }
+            if(screenPos2.x < innerBounds.x+innerBounds.width && i == (state->numPoints-2)){
+                DrawLine(screenPos2.x, screenPos2.y, innerBounds.x+innerBounds.width, screenPos2.y, curveColor);
+            }
+            // Draw cubic Hermite curve
+            const float scale = (p2->position.x-p1->position.x)/3.f;
+            const Vector2 offset1 = (Vector2) {scale, scale*p1->tangents.y};
+            // negative endTangent => top part => need to invert value to calculate offset
+            const Vector2 offset2 = (Vector2) {-scale, -scale*p2->tangents.x};
+
+            const Vector2 c1 = (Vector2) {p1->position.x+offset1.x, p1->position.y+offset1.y};
+            const Vector2 c2 = (Vector2) {p2->position.x+offset2.x, p2->position.y+offset2.y};
+
+            const Vector2 screenC1 = (Vector2) {c1.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-c1.y*innerBounds.height};
+            const Vector2 screenC2 = (Vector2) {c2.x*innerBounds.width+innerBounds.x, innerBounds.y+innerBounds.height-c2.y*innerBounds.height};
+
+            DrawLineBezierCubic(screenPos1, screenPos2, screenC1, screenC2, 1, curveColor);
+        }
+    }
+}
+
+#endif // GUI_CURVE_EDIT_IMPLEMENTATION
+