Skip to content

Shape Modelling

This guide introduces the functions for managing shapes in graphical applications built with SplashKit. We will discuss how to use the API in SplashKit to create graphical application tools.
Written by: Yuyang Yang and Vishnu
Last updated: Aug 20 2025


In this guide, you’ll learn how to create and manage graphics in SplashKit, including shape creation, movement, resizing, and connecting shapes with lines.

In this section, we’ll detail how to implement the shape creation and management functionality using design patterns and code examples.

We start by defining a ShapeModel structure to represent various basic shapes. This model holds attributes like position, size, color, and shape type.

struct ShapeModel
{
double x, y; // Shape's starting position
double width, height; // Shape's width and height
string color; // Shape's color
double rotation; // Shape's rotation angle
string type; // Shape type (Rectangle, Circle, Triangle, etc.)
};

The ShapeModel can represent various shapes like rectangles, triangles, etc. The type field is used to distinguish between different shapes, allowing the correct drawing function to be called during rendering.

The code simulates the user selecting a graphic and placing it on the canvas. When the user clicks on the canvas, the code creates a new graphic at the mouse position based on the preset shape type

if (mouse_clicked(LEFT_BUTTON))
{
ShapeModel new_shape;
new_shape.x = mouse_x(); // Get the X coordinate of the mouse
new_shape.y = mouse_y(); // Get the Y coordinate of the mouse
new_shape.width = 100; // Set default width
new_shape.height = 100; // Set default height
new_shape.color = "Red"; // Set default color
new_shape.type = "Rectangle"; // Set default type to Rectangle
shapes.push_back(new_shape); // Add the new shape to the shapes container
}

We can manage rendering by iterating through the shape collection and calling the appropriate drawing function based on the shape’s type.

clear_screen(COLOR_WHITE);
for (int i = 0; i < shapes.size(); i++)
{
fill_rectangle(COLOR_RED, shapes[i].x, shapes[i].y, shapes[i].width, shapes[i].height);
}
refresh_screen(60);
#include "splashkit.h"
#include <vector>
struct ShapeModel
{
double x, y;
double width, height;
string color;
double rotation;
string type;
};
int main()
{
open_window("Drawing Test", 800, 600);
vector<ShapeModel> shapes;
while (!window_close_requested("Drawing Test"))
{
process_events();
if (mouse_clicked(LEFT_BUTTON))
{
ShapeModel new_shape;
new_shape.x = mouse_x();
new_shape.y = mouse_y();
new_shape.width = 100;
new_shape.height = 100;
new_shape.color = "Red";
new_shape.type = "Rectangle";
shapes.push_back(new_shape);
}
clear_screen(COLOR_WHITE);
for (int i = 0; i < shapes.size(); i++)
{
fill_rectangle(COLOR_RED, shapes[i].x, shapes[i].y, shapes[i].width, shapes[i].height);
}
refresh_screen(60);
}
close_all_windows()
return 0;
}

In this section, we’ll discuss how to handle the selection and movement of shapes. The code is designed to allow users to select shapes, move them around the canvas, and resize them. This approach makes the code more modular, easier to maintain, and extend.

First, we need to determine which shape the user has clicked on. This is achieved by checking if the mouse click position falls within the bounds of any existing shape. The get_shape_at() function is used to detect whether the mouse click is within a shape, allowing the program to identify which shape, if any, the user has selected.

ShapeModel* get_shape_at(vector<ShapeModel>& shapes, double x, double y)
{
for (int i = 0; i < shapes.size(); i++)
{
if (shapes[i].type == "Rectangle" && x >= shapes[i].x && x <= (shapes[i].x + shapes[i].width) && y >= shapes[i].y && y <= (shapes[i].y + shapes[i].height))
{
return &shapes[i];
}
}
return nullptr;
}

Once a shape is selected, the user can drag it by moving the mouse. This is done by updating the shape’s x and y coordinates to follow the mouse’s movement. The is_moving flag is used to distinguish between creating a new shape and moving an existing shape.

if (is_moving && selected_shape != nullptr)
{
selected_shape->x = mouse_x() - selected_shape->width / 2;
selected_shape->y = mouse_y() - selected_shape->height / 2;
}

In addition to dragging, users can resize shapes. Holding down the SPACE key while clicking a shape activates resizing mode. The shape’s width and height update dynamically based on the mouse position.

if (is_resizing && selected_shape != nullptr)
{
selected_shape->width = mouse_x() - selected_shape->x;
selected_shape->height = mouse_y() - selected_shape->y;
}

When the user releases the mouse button, the dragging or resizing operation stops, and the shape’s final position or size is confirmed.

#include "splashkit.h"
#include <vector>
struct ShapeModel
{
double x, y;
double width, height;
string color;
double rotation;
string type;
};
ShapeModel *get_shape_at(vector<ShapeModel> &shapes, double x, double y)
{
for (int i = 0; i < shapes.size(); i++)
{
if (shapes[i].type == "Rectangle" && x >= shapes[i].x && x <= (shapes[i].x + shapes[i].width) && y >= shapes[i].y && y <= (shapes[i].y + shapes[i].height))
{
return &shapes[i];
}
}
return nullptr;
}
int main()
{
open_window("Drawing Test", 800, 600);
vector<ShapeModel> shapes;
ShapeModel *selected_shape = nullptr;
bool is_moving = false;
bool is_resizing = false;
while (!window_close_requested("Drawing Test"))
{
process_events();
if (!is_moving && !is_resizing)
{
if (mouse_down(LEFT_BUTTON))
{
selected_shape = get_shape_at(shapes, mouse_x(), mouse_y());
if (selected_shape != nullptr)
{
if (key_down(SPACE_KEY))
{
is_resizing = true;
}
else
{
is_moving = true;
}
}
else
{
ShapeModel new_shape;
new_shape.x = mouse_x();
new_shape.y = mouse_y();
new_shape.width = 100;
new_shape.height = 100;
new_shape.color = "Red";
new_shape.type = "Rectangle";
shapes.push_back(new_shape);
}
}
}
if (is_moving && selected_shape != nullptr)
{
selected_shape->x = mouse_x() - selected_shape->width / 2;
selected_shape->y = mouse_y() - selected_shape->height / 2;
}
if (is_resizing && selected_shape != nullptr)
{
selected_shape->width = mouse_x() - selected_shape->x;
selected_shape->height = mouse_y() - selected_shape->y;
}
if (mouse_up(LEFT_BUTTON))
{
is_moving = false;
is_resizing = false;
selected_shape = nullptr;
}
clear_screen(COLOR_WHITE);
for (int i = 0; i < shapes.size(); i++)
{
if (shapes[i].type == "Rectangle")
{
fill_rectangle(COLOR_RED, shapes[i].x, shapes[i].y, shapes[i].width, shapes[i].height);
}
}
refresh_screen(60);
}
return 0;
}

In this section, we’ll add connection lines so users can visually link two shapes. Here’s how to implement it.

In this feature, we use the state mode to handle the connections between shapes. State mode allows us to perform different operations in different states, such as connection mode and normal mode. Encapsulating the connection state in a class or function makes the process clearer and allows easy switching between modes.

We first define a basic shape structure ShapeModel, which contains properties such as x, y coordinates, width, height, color, and type.

We use the get_shape_at function to detect whether the mouse is over a shape. It loops through the list of shapes and checks if the mouse coordinates fall within any shape’s boundaries.

To start connecting shapes, we press the C key. This switches the program into connection mode, allowing the user to choose two shapes to connect with a line.

if (key_down(C_KEY) && !is_connecting)
{
is_connecting = true;
connection_start = nullptr;
}

While in connection mode, when the user clicks on a shape, it records the starting point (connection_start). The next click sets the end point (connection_end). Once both are selected, a connection is stored.

if (is_connecting)
{
if (mouse_down(LEFT_BUTTON))
{
if (connection_start == nullptr)
{
connection_start = get_shape_at(shapes, mouse_x(), mouse_y());
}
else
{
ShapeModel* connection_end = get_shape_at(shapes, mouse_x(), mouse_y());
if (connection_end != nullptr && connection_end != connection_start)
{
connections.push_back({ connection_start, connection_end });
is_connecting = false; // End connection mode
connection_start = nullptr;
}
}
}
}

Using the draw_line function, we draw lines between the center points of the two selected shapes. This line is visually updated in real time during the connection process.

for (int i = 0; i < connections.size(); i++)
{
draw_line(COLOR_RED, connections[i].start->x + connections[i].start->width / 2, connections[i].start->y + connections[i].start->height / 2, connections[i].end->x + connections[i].end->width / 2, connections[i].end->y + connections[i].end->height / 2);
}

After a connection is drawn, the program exits connection mode and returns to normal interaction.

This implementation creates an intuitive and interactive way for users to connect shapes, simulating links between objects.

#include "splashkit.h"
#include <vector>
struct ShapeModel
{
double x, y;
double width, height;
string color;
double rotation;
string type;
};
struct Connection
{
ShapeModel *start;
ShapeModel *end;
};
ShapeModel *get_shape_at(vector<ShapeModel> &shapes, double x, double y)
{
for (int i = 0; i < shapes.size(); i++)
{
if (shapes[i].type == "Rectangle" && x >= shapes[i].x && x <= (shapes[i].x + shapes[i].width) && y >= shapes[i].y && y <= (shapes[i].y + shapes[i].height))
{
return &shapes[i];
}
}
return nullptr;
}
int main()
{
open_window("Drawing Test", 800, 600);
vector<ShapeModel> shapes;
vector<Connection> connections;
ShapeModel *selected_shape = nullptr;
ShapeModel *connection_start = nullptr;
bool is_moving = false;
bool is_resizing = false;
bool is_connecting = false;
while (!window_close_requested("Drawing Test"))
{
process_events();
if (key_down(C_KEY) && !is_connecting)
{
is_connecting = true;
connection_start = nullptr;
}
if (is_connecting)
{
if (mouse_down(LEFT_BUTTON))
{
if (connection_start == nullptr)
{
connection_start = get_shape_at(shapes, mouse_x(), mouse_y());
}
else
{
ShapeModel *connection_end = get_shape_at(shapes, mouse_x(), mouse_y());
if (connection_end != nullptr && connection_end != connection_start)
{
Connection connection = {connection_start, connection_end};
connections.push_back(connection);
is_connecting = false;
connection_start = nullptr;
}
}
}
}
else if (!is_moving && !is_resizing)
{
if (mouse_down(LEFT_BUTTON))
{
selected_shape = get_shape_at(shapes, mouse_x(), mouse_y());
if (selected_shape != nullptr)
{
if (key_down(SPACE_KEY))
{
is_resizing = true;
}
else
{
is_moving = true;
}
}
else
{
ShapeModel new_shape;
new_shape.x = mouse_x();
new_shape.y = mouse_y();
new_shape.width = 100;
new_shape.height = 100;
new_shape.color = "Red";
new_shape.type = "Rectangle";
shapes.push_back(new_shape);
}
}
}
if (is_moving && selected_shape != nullptr)
{
selected_shape->x = mouse_x() - selected_shape->width / 2;
selected_shape->y = mouse_y() - selected_shape->height / 2;
}
if (is_resizing && selected_shape != nullptr)
{
selected_shape->width = mouse_x() - selected_shape->x;
selected_shape->height = mouse_y() - selected_shape->y;
}
if (mouse_up(LEFT_BUTTON))
{
is_moving = false;
is_resizing = false;
selected_shape = nullptr;
}
clear_screen(COLOR_WHITE);
for (int i = 0; i < shapes.size(); i++)
{
if (shapes[i].type == "Rectangle")
{
fill_rectangle(COLOR_RED, shapes[i].x, shapes[i].y, shapes[i].width, shapes[i].height);
}
}
if (is_connecting && connection_start != nullptr)
{
draw_line(COLOR_BLACK, connection_start->x + connection_start->width / 2, connection_start->y + connection_start->height / 2, mouse_x(), mouse_y());
}
for (int i = 0; i < connections.size(); i++)
{
draw_line(COLOR_RED, connections[i].start->x + connections[i].start->width / 2, connections[i].start->y + connections[i].start->height / 2, connections[i].end->x + connections[i].end->width / 2, connections[i].end->y + connections[i].end->height / 2);
}
refresh_screen(60);
}
return 0;
}