/******************************************************************************************* * * raylib [shapes] example - splines drawing * * Example complexity rating: [★★★☆] 3/4 * * Example originally created with raylib 5.0, last time updated with raylib 5.0 * * Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, * BSD-like license that allows static linking with closed source software * * Copyright (c) 2023-2025 Ramon Santamaria (@raysan5) * ********************************************************************************************/ #include "raylib.h" #include "raymath.h" #define RAYGUI_IMPLEMENTATION #include "raygui.h" // Required for UI controls #include // Required for: NULL #define MAX_SPLINE_POINTS 32 #define SPLINE_THICK_COUNT 4 // Cubic Bezier spline control points // NOTE: Every segment has two control points typedef struct { Vector2 start; Vector2 end; } ControlPoint; // Spline types typedef enum { SPLINE_LINEAR = 0, // Linear SPLINE_BASIS, // B-Spline SPLINE_CATMULLROM, // Catmull-Rom SPLINE_BEZIER, // Cubic Bezier SPLINE_LINEAR_VAR, // Linear, variable thickness SPLINE_BEZIER_VAR // Cubic Bezier, variable thickness } SplineType; //------------------------------------------------------------------------------------ // Program main entry point //------------------------------------------------------------------------------------ int main(void) { // Initialization //-------------------------------------------------------------------------------------- const int screenWidth = 800; const int screenHeight = 450; SetConfigFlags(FLAG_MSAA_4X_HINT); InitWindow(screenWidth, screenHeight, "raylib [shapes] example - splines drawing"); Vector2 points[MAX_SPLINE_POINTS] = { { 50.0f, 400.0f }, { 160.0f, 220.0f }, { 340.0f, 380.0f }, { 520.0f, 60.0f }, { 710.0f, 260.0f }, }; float thicks[SPLINE_THICK_COUNT] = { 0.0f, 8.0f, 8.0f, 0.0f, }; // Array required for spline bezier-cubic, // including control points interleaved with start-end segment points Vector2 pointsInterleaved[3*(MAX_SPLINE_POINTS - 1) + 1] = { 0 }; int pointCount = 5; int selectedPoint = -1; int focusedPoint = -1; int selectedThickPoint = -1; int focusedThickPoint = -1; Vector2 *selectedControlPoint = NULL; Vector2 *focusedControlPoint = NULL; // Cubic Bezier control points initialization ControlPoint control[MAX_SPLINE_POINTS - 1] = { 0 }; for (int i = 0; i < pointCount - 1; i++) { control[i].start = (Vector2){ points[i].x + 50, points[i].y }; control[i].end = (Vector2){ points[i + 1].x - 50, points[i + 1].y }; } // Spline config variables float splineThickness = 8.0f; int splineTypeActive = SPLINE_LINEAR; // 0-Linear, 1-BSpline, 2-CatmullRom, 3-Bezier, 4-LinearVar, 5-BezierVar bool splineTypeEditMode = false; bool splineHelpersActive = true; SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- // Main game loop while (!WindowShouldClose()) // Detect window close button or ESC key { // Update //---------------------------------------------------------------------------------- // Spline points creation logic (at the end of spline) if (IsMouseButtonPressed(MOUSE_RIGHT_BUTTON) && (pointCount < MAX_SPLINE_POINTS)) { points[pointCount] = GetMousePosition(); int i = pointCount - 1; control[i].start = (Vector2){ points[i].x + 50, points[i].y }; control[i].end = (Vector2){ points[i + 1].x - 50, points[i + 1].y }; pointCount++; } // Spline point focus and selection logic if ((selectedPoint == -1) && ((splineTypeActive != SPLINE_BEZIER) || (selectedControlPoint == NULL))) { focusedPoint = -1; for (int i = 0; i < pointCount; i++) { if (CheckCollisionPointCircle(GetMousePosition(), points[i], 8.0f)) { focusedPoint = i; break; } } if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) selectedPoint = focusedPoint; } // Spline point movement logic if (selectedPoint >= 0) { points[selectedPoint] = GetMousePosition(); if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) selectedPoint = -1; } // Cubic Bezier spline control points logic if (((splineTypeActive == SPLINE_BEZIER) || (splineTypeActive == SPLINE_BEZIER_VAR)) && (focusedPoint == -1)) { // Spline control point focus and selection logic if (selectedControlPoint == NULL) { focusedControlPoint = NULL; for (int i = 0; i < pointCount - 1; i++) { if (CheckCollisionPointCircle(GetMousePosition(), control[i].start, 6.0f)) { focusedControlPoint = &control[i].start; break; } else if (CheckCollisionPointCircle(GetMousePosition(), control[i].end, 6.0f)) { focusedControlPoint = &control[i].end; break; } } if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) selectedControlPoint = focusedControlPoint; } // Spline control point movement logic if (selectedControlPoint != NULL) { *selectedControlPoint = GetMousePosition(); if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) selectedControlPoint = NULL; } } // Variable thickness control points logic if ((splineTypeActive == SPLINE_LINEAR_VAR) && focusedPoint == -1) { // Spline thickness control point focus and selection logic if (selectedThickPoint == -1) { focusedThickPoint = -1; for (int i = 0; i < pointCount - 1; i++) { Vector2 direction = Vector2Normalize(GetSplineVelocityLinear(points[i], points[i + 1])); Vector2 perpendicular = (Vector2){ direction.y, -direction.x }; for (int j = 0; j < SPLINE_THICK_COUNT; j++) { Vector2 point = Vector2Add(GetSplinePointLinear(points[i], points[i + 1], (float)j/(SPLINE_THICK_COUNT - 1)), Vector2Scale(perpendicular, thicks[j])); if (CheckCollisionPointCircle(GetMousePosition(), point, 4.0f)) { focusedThickPoint = i*SPLINE_THICK_COUNT + j; break; } } } if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) selectedThickPoint = focusedThickPoint; } if (selectedThickPoint >= 0) { int i = selectedThickPoint/SPLINE_THICK_COUNT; int j = selectedThickPoint%SPLINE_THICK_COUNT; Vector2 direction = Vector2Normalize(GetSplineVelocityLinear(points[i], points[i + 1])); Vector2 perpendicular = (Vector2){ direction.y, -direction.x }; thicks[j] = Vector2DotProduct(perpendicular, Vector2Subtract(GetMousePosition(), points[i])); if (thicks[j] > 40.0f) thicks[j] = 40.0f; if (thicks[j] < -40.0f) thicks[j] = -40.0f; if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) selectedThickPoint = -1; } } // Spline selection logic if (IsKeyPressed(KEY_ONE)) splineTypeActive = SPLINE_LINEAR; else if (IsKeyPressed(KEY_TWO)) splineTypeActive = SPLINE_BASIS; else if (IsKeyPressed(KEY_THREE)) splineTypeActive = SPLINE_CATMULLROM; else if (IsKeyPressed(KEY_FOUR)) splineTypeActive = SPLINE_BEZIER; else if (IsKeyPressed(KEY_FIVE)) splineTypeActive = SPLINE_LINEAR_VAR; else if (IsKeyPressed(KEY_SIX)) splineTypeActive = SPLINE_BEZIER_VAR; // Clear selection when changing to a spline without control points if (IsKeyPressed(KEY_ONE) || IsKeyPressed(KEY_TWO) || IsKeyPressed(KEY_THREE)) selectedControlPoint = NULL; //---------------------------------------------------------------------------------- // Draw //---------------------------------------------------------------------------------- BeginDrawing(); ClearBackground(RAYWHITE); if (splineTypeActive == SPLINE_LINEAR) { // Draw spline: linear DrawSplineLinear(points, pointCount, splineThickness, RED); } else if (splineTypeActive == SPLINE_BASIS) { // Draw spline: basis DrawSplineBasis(points, pointCount, splineThickness, RED); // Provide connected points array /* for (int i = 0; i < (pointCount - 3); i++) { // Drawing individual segments, not considering thickness connection compensation DrawSplineSegmentBasis(points[i], points[i + 1], points[i + 2], points[i + 3], splineThickness, MAROON); } */ } else if (splineTypeActive == SPLINE_CATMULLROM) { // Draw spline: catmull-rom DrawSplineCatmullRom(points, pointCount, splineThickness, RED); // Provide connected points array /* for (int i = 0; i < (pointCount - 3); i++) { // Drawing individual segments, not considering thickness connection compensation DrawSplineSegmentCatmullRom(points[i], points[i + 1], points[i + 2], points[i + 3], splineThickness, MAROON); } */ } else if (splineTypeActive == SPLINE_BEZIER) { // NOTE: Cubic-bezier spline requires the 2 control points of each segnment to be // provided interleaved with the start and end point of every segment for (int i = 0; i < (pointCount - 1); i++) { pointsInterleaved[3*i] = points[i]; pointsInterleaved[3*i + 1] = control[i].start; pointsInterleaved[3*i + 2] = control[i].end; } pointsInterleaved[3*(pointCount - 1)] = points[pointCount - 1]; // Draw spline: cubic-bezier (with control points) DrawSplineBezierCubic(pointsInterleaved, 3*(pointCount - 1) + 1, splineThickness, RED); /* for (int i = 0; i < 3*(pointCount - 1); i += 3) { // Drawing individual segments, not considering thickness connection compensation DrawSplineSegmentBezierCubic(pointsInterleaved[i], pointsInterleaved[i + 1], pointsInterleaved[i + 2], pointsInterleaved[i + 3], splineThickness, MAROON); } */ } else if (splineTypeActive == SPLINE_LINEAR_VAR) { // Draw spline: variable-width linear for (int i = 0; i < pointCount - 1; ++i) { DrawSplineSegmentLinearVar(points[i], points[i + 1], thicks, SPLINE_THICK_COUNT, RED); } } else if (splineTypeActive == SPLINE_BEZIER_VAR) { // NOTE: Cubic-bezier spline requires the 2 control points of each segnment to be // provided interleaved with the start and end point of every segment for (int i = 0; i < (pointCount - 1); i++) { pointsInterleaved[3*i] = points[i]; pointsInterleaved[3*i + 1] = control[i].start; pointsInterleaved[3*i + 2] = control[i].end; } pointsInterleaved[3*(pointCount - 1)] = points[pointCount - 1]; // Draw spline: variable-width cubic-bezier (with control points) for (int i = 0; i < pointCount - 1; ++i) { DrawSplineSegmentBezierCubicVar(points[i], control[i].start, control[i].end, points[i + 1], thicks, SPLINE_THICK_COUNT, RED); } } if ((splineTypeActive == SPLINE_BEZIER) || (splineTypeActive == SPLINE_BEZIER_VAR)) { // Draw spline control points for (int i = 0; i < pointCount - 1; i++) { // Every cubic bezier point have two control points DrawCircleV(control[i].start, 6, GOLD); DrawCircleV(control[i].end, 6, GOLD); if (focusedControlPoint == &control[i].start) DrawCircleV(control[i].start, 8, GREEN); else if (focusedControlPoint == &control[i].end) DrawCircleV(control[i].end, 8, GREEN); DrawLineEx(points[i], control[i].start, 1.0f, LIGHTGRAY); DrawLineEx(points[i + 1], control[i].end, 1.0f, LIGHTGRAY); // Draw spline control lines DrawLineV(points[i], control[i].start, GRAY); //DrawLineV(control[i].start, control[i].end, LIGHTGRAY); DrawLineV(control[i].end, points[i + 1], GRAY); } } if (splineHelpersActive) { // Draw spline point helpers for (int i = 0; i < pointCount; i++) { DrawCircleLinesV(points[i], (focusedPoint == i)? 12.0f : 8.0f, (focusedPoint == i)? BLUE: DARKBLUE); if ((splineTypeActive != SPLINE_LINEAR) && (splineTypeActive != SPLINE_LINEAR_VAR) && (splineTypeActive != SPLINE_BEZIER) && (splineTypeActive != SPLINE_BEZIER_VAR) && (i < pointCount - 1)) DrawLineV(points[i], points[i + 1], GRAY); DrawText(TextFormat("[%.0f, %.0f]", points[i].x, points[i].y), (int)points[i].x, (int)points[i].y + 10, 10, BLACK); } // Draw spline thickness helpers if (splineTypeActive == SPLINE_LINEAR_VAR) { Vector2 thickPoints[SPLINE_THICK_COUNT] = { 0 }; for (int i = 0; i < pointCount - 1; i++) { Vector2 direction = Vector2Normalize(GetSplineVelocityLinear(points[i], points[i + 1])); Vector2 perpendicular = (Vector2){ direction.y, -direction.x }; DrawLineV(points[i], points[i + 1], BROWN); for (int j = 0; j < SPLINE_THICK_COUNT; j++) { Vector2 anchor = GetSplinePointLinear(points[i], points[i + 1], (float)j/(SPLINE_THICK_COUNT - 1)); thickPoints[j] = Vector2Add(anchor, Vector2Scale(perpendicular, thicks[j])); DrawLineV( Vector2Add(anchor, Vector2Scale(perpendicular, 4.0f)), Vector2Subtract(anchor, Vector2Scale(perpendicular, 4.0f)), GRAY); DrawCircleLinesV(thickPoints[j], (((focusedThickPoint % SPLINE_THICK_COUNT) == j)? 6.0f : 4.0f), (((focusedThickPoint % SPLINE_THICK_COUNT) == j)? VIOLET : PURPLE)); } DrawSplineBezierCubic(thickPoints, SPLINE_THICK_COUNT, 1.0f, ORANGE); DrawLineV(thickPoints[0], thickPoints[1], GRAY); DrawLineV(thickPoints[3], thickPoints[2], GRAY); } } else if (splineTypeActive == SPLINE_BEZIER_VAR) { // I tried and this isn't possible. // A variable-thickness Bezier curve can potentially have more detail than can // be expressed with another Bezier curve of the same degree. // Example: https://www.desmos.com/calculator/qh31vr4rbf } } // Check all possible UI states that require controls lock if (splineTypeEditMode || (selectedPoint != -1) || (selectedThickPoint != -1) || (selectedControlPoint != NULL)) GuiLock(); // Draw spline config if ((splineTypeActive == SPLINE_LINEAR_VAR) || (splineTypeActive == SPLINE_BEZIER_VAR)) { GuiLabel((Rectangle){ 12, 68 + 24, 200, 24 }, TextFormat("Spline thickness: %i, %i, %i, %i", (int)thicks[0], (int)thicks[1], (int)thicks[2], (int)thicks[3])); GuiSlider((Rectangle){ 12, 68 + 24 + 30, 140, 16 }, NULL, NULL, &thicks[0], -40.0, 40.0f); GuiSlider((Rectangle){ 12, 68 + 24 + 50, 140, 16 }, NULL, NULL, &thicks[1], -40.0, 40.0f); GuiSlider((Rectangle){ 12, 68 + 24 + 70, 140, 16 }, NULL, NULL, &thicks[2], -40.0, 40.0f); GuiSlider((Rectangle){ 12, 68 + 24 + 90, 140, 16 }, NULL, NULL, &thicks[3], -40.0, 40.0f); } else { GuiLabel((Rectangle){ 12, 68 + 24, 140, 24 }, TextFormat("Spline thickness: %i", (int)splineThickness)); GuiSliderBar((Rectangle){ 12, 68 + 48, 140, 16 }, NULL, NULL, &splineThickness, 1.0f, 40.0f); } GuiCheckBox((Rectangle){ 12, 68, 20, 20 }, "Show point helpers", &splineHelpersActive); if (splineTypeEditMode) GuiUnlock(); GuiLabel((Rectangle){ 12, 10, 140, 24 }, "Spline type:"); if (GuiDropdownBox((Rectangle){ 12, 8 + 24, 140, 28 }, "LINEAR;BSPLINE;CATMULLROM;BEZIER;LINEAR VARIABLE;BEZIER VARIABLE", &splineTypeActive, splineTypeEditMode)) splineTypeEditMode = !splineTypeEditMode; GuiUnlock(); EndDrawing(); //---------------------------------------------------------------------------------- } // De-Initialization //-------------------------------------------------------------------------------------- CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; }