From 1d9fd31074ce9ab06391e934cd4bc351475924d1 Mon Sep 17 00:00:00 2001
From: Pierre Jaffuer
Date: Sat, 8 Apr 2023 21:14:40 +0400
Subject: [PATCH] Added a new example: animation curves (#268)
* A somewhat advanced example with custom curve controls.
* Name update
* typos
* comments
---
examples/Makefile | 3 +-
examples/animation_curve/animation_curve.c | 480 +++++++++++++++++++
examples/animation_curve/gui_curve_edit.h | 517 +++++++++++++++++++++
3 files changed, 999 insertions(+), 1 deletion(-)
create mode 100644 examples/animation_curve/animation_curve.c
create mode 100644 examples/animation_curve/gui_curve_edit.h
diff --git a/examples/Makefile b/examples/Makefile
index 7496453..150c7b2 100644
--- a/examples/Makefile
+++ b/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))
diff --git a/examples/animation_curve/animation_curve.c b/examples/animation_curve/animation_curve.c
new file mode 100644
index 0000000..7e8d28a
--- /dev/null
+++ b/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;
+}
+
+
diff --git a/examples/animation_curve/gui_curve_edit.h b/examples/animation_curve/gui_curve_edit.h
new file mode 100644
index 0000000..0a726f1
--- /dev/null
+++ b/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
+