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 +