Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*
- Let's implement a simple graphic editor.
- Main features:
- * Crossplatflorm, written in C++ and SDL2.
- * Image is fixed size 640x640 monochrome(0x111111 black background, 0xEEEEEE white foreground by default)
- * It has layers(no groups, starts with a single layer).
- * Each layer consists of strokes.
- * Each layer has offset (dx, dy), zero by default
- * Each strokes consists of connected points.
- Command handler: add a new stroke
- * When user clicks with LMB, a command is initiated: a new stroke starts.
- * As user moves the mouse around with LMB pressed, the stroke is extended towards the mouse position.
- * If LMB is lifted, stroke is added to the current layer
- * If RMB is clicked when stroke is being drawn, we count it as a cancel. The stroke is removed and we wait until both LMB and RMB are lifted.
- Command handler: split-end
- * If user presses single letter 'S', a command is initiated: a new split starts. User has to choose how many (last) strokes to move to the new layer.
- * If user scrolls the wheel up, more strokes are being selected(one by one), starting from the last strokes.
- * If user scrolls the wheel down, deselect most recently selected strokes. (Last stroke to be selected - first stroke to be deselected)
- * In this mode strokes instead of white have two colors:
- * non-selected strokes: dull white (around 0x777777)
- * selected strokes: red (0xC44141)
- * If user clicks RMB, operation is cancelled
- * If user clicks LMB, selected strokes are removed from the selected layer and moved to the new one, after the previously selected layer. The new layer is selected.
- * If all strokes are selected, no new layer is being created, current layer is being reselected
- Command handler: split-beginning
- * If user presses Shift-S, a command is initiated: a new split at the beginning starts. User has to choos how many strokes at the beginning of the layer to select as in split-end.
- * If user scrolls the wheel up, more strokes are being selected(one by one), starting from the first strokes.
- * If user scrolls the wheel down, deselect most recently selected strokes. (Last stroke to be selected - first stroke to be deselected)
- * In this mode strokes instead of white have two colors:
- * non-selected strokes: dull white (around 0x777777)
- * selected strokes: red (0xC44141)
- * If user clicks RMB, operation is cancelled
- * If user clicks LMB, selected strokes are removed from the selected layer and moved to the new one, before the previously selected layer. The new layer is selected.
- * If all strokes are selected, no new layer is being created, current layer is being reselected
- Command handler: select-layer.
- * When a layer is selected, its stroke color is changed to 0xFFFFFF. If no strokes exists, change background to 0x333333 (change it back to 0x111111 once a stroke is added or other layer with stroke is selected)
- * Pressing `tab` moves cursor to the next layer if it exists.
- * if it doesn't, selects the first layer
- * Pressing `shift-tab` moves cursor to the previous layer if it exists.
- * if it doesn't, selects the last layer
- Command handler: new-layer
- * If user presses 'Ins', create a new layer after the current one and select it
- * If user presses 'Del', delete a current layer and select the previous one
- Command handler: transpose-layer
- * If user presses middle mouse, start to transpose the layer: if cursor is moved from (x0, y0) to (x1, y1), then change layer offset(change dx, dy of the layer)
- * If middle mouse is released, end the layer transposing.
- * If right mouse is clicked, cancel the operation.
- Take offset into account:
- * When layer is rendered, take its offset dx,dy into account. E.g. if its stroke goes (0,0)->(10,10) and dx,dy=(1,2), then rendered stroke should be (1,2)->(11,12)
- * When you add stroke to a layer, make sure you take applied offset into account. I.e if dx,dy=100,0 and user draws line (100,0)->(105,0) on screen, then in reality it's a stroke (0,0)->(5,0)
- Implementation details:
- for a stroke feel free to use struct with std::vector<SDL_Point>
- for a layer feel free to use struct with std::deque<Stroke>
- Not required:
- Undo,redo,save,load
- */
- #include <SDL2/SDL.h>
- #include <vector>
- #include <deque>
- #include <iostream>
- #include <numeric>
- #include <algorithm>
- // --- Configuration ---
- const int SCREEN_WIDTH = 640;
- const int SCREEN_HEIGHT = 640;
- const SDL_Color COLOR_BACKGROUND = {0x11, 0x11, 0x11, 0xFF};
- const SDL_Color COLOR_BACKGROUND_SELECTED_EMPTY = {0x33, 0x33, 0x33, 0xFF};
- const SDL_Color COLOR_FOREGROUND = {0xDD, 0xDD, 0xDD, 0xFF};
- const SDL_Color COLOR_FOREGROUND_SELECTED = {0xDD, 0xFF, 0xDD, 0xFF};
- const SDL_Color COLOR_SPLIT_DULL = {0x77, 0x77, 0x77, 0xFF};
- const SDL_Color COLOR_SPLIT_SELECTED = {0xC4, 0x41, 0x41, 0xFF};
- // --- Data Structures ---
- // Represents a single continuous line drawn by the user.
- // Honestly, it's just a bunch of points. Don't overthink it.
- struct Stroke {
- std::vector<SDL_Point> points;
- };
- // A layer contains multiple strokes and has an offset.
- // Think of it like transparent sheets you stack. Duh.
- struct Layer {
- std::deque<Stroke> strokes;
- SDL_Point offset = {0, 0}; // How much this layer is shifted.
- };
- // --- Application State ---
- enum class CommandState {
- IDLE,
- DRAWING_STROKE,
- TRANSPOSING_LAYER,
- SPLITTING_END,
- SPLITTING_BEGINNING
- };
- // --- Helper Functions ---
- // Set SDL draw color using our SDL_Color struct.
- // Saves you like, two lines of code. You're welcome. 😒
- void SetRenderDrawColor(SDL_Renderer* renderer, const SDL_Color& color) {
- SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
- }
- // Transform screen coordinates to world coordinates for the selected layer.
- // Basically, undoing the layer's offset. Pay attention!
- SDL_Point ScreenToWorld(const SDL_Point& screenPoint, const SDL_Point& layerOffset) {
- return {screenPoint.x - layerOffset.x, screenPoint.y - layerOffset.y};
- }
- // Transform world coordinates (relative to layer) to screen coordinates.
- // Applying the layer's offset. Simple, right?
- SDL_Point WorldToScreen(const SDL_Point& worldPoint, const SDL_Point& layerOffset) {
- return {worldPoint.x + layerOffset.x, worldPoint.y + layerOffset.y};
- }
- // Draw a single stroke, applying the layer's offset.
- // Takes the points in the stroke, adds the offset, then draws lines.
- void DrawStroke(SDL_Renderer* renderer, const Stroke& stroke, const SDL_Point& layerOffset, const SDL_Color& color) {
- if (stroke.points.size() < 2) {
- // Can't draw a line with less than 2 points, obviously.
- if (stroke.points.size() == 1) {
- // Draw a single point if that's all there is.
- SDL_Point screenPoint = WorldToScreen(stroke.points[0], layerOffset);
- SetRenderDrawColor(renderer, color);
- SDL_RenderDrawPoint(renderer, screenPoint.x, screenPoint.y);
- }
- return;
- }
- // Convert world points to screen points for drawing
- std::vector<SDL_Point> screenPoints(stroke.points.size());
- std::transform(stroke.points.begin(), stroke.points.end(), screenPoints.begin(),
- [&](const SDL_Point& p) { return WorldToScreen(p, layerOffset); });
- SetRenderDrawColor(renderer, color);
- SDL_RenderDrawLines(renderer, screenPoints.data(), static_cast<int>(screenPoints.size()));
- }
- // --- Main Application Logic ---
- int main(int argc, char* argv[]) {
- // --- SDL Initialization ---
- // Boring boilerplate. If this fails, it's probably your fault. 💢
- if (SDL_Init(SDL_INIT_VIDEO) < 0) {
- std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl;
- return 1;
- }
- SDL_Window* window = SDL_CreateWindow("Tsundere Paint (It's not like I made it for YOU!)",
- SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
- SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
- if (!window) {
- std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << std::endl;
- SDL_Quit();
- return 1;
- }
- SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
- if (!renderer) {
- std::cerr << "Renderer could not be created! SDL Error: " << SDL_GetError() << std::endl;
- SDL_DestroyWindow(window);
- SDL_Quit();
- return 1;
- }
- // --- Application Data ---
- std::vector<Layer> layers;
- layers.push_back({}); // Start with one empty layer. How generous of me.
- size_t selected_layer_index = 0;
- CommandState current_state = CommandState::IDLE;
- // State for drawing
- Stroke current_stroke;
- bool lmb_down = false;
- bool rmb_down = false; // Track RMB separately for cancellation
- // State for transposing
- SDL_Point transpose_start_pos = {0, 0};
- SDL_Point original_layer_offset = {0, 0};
- bool mmb_down = false;
- // State for splitting
- int split_count = 0; // How many strokes are selected for splitting
- // --- Main Loop ---
- bool quit = false;
- SDL_Event e;
- while (!quit) {
- // --- Event Handling ---
- // Pay attention here, this is where things happen!
- while (SDL_PollEvent(&e) != 0) {
- if (e.type == SDL_QUIT) {
- quit = true;
- }
- // --- Mouse Button Events ---
- else if (e.type == SDL_MOUSEBUTTONDOWN) {
- SDL_Point mousePos = {e.button.x, e.button.y};
- if (e.button.button == SDL_BUTTON_LEFT) {
- lmb_down = true;
- if (current_state == CommandState::IDLE) {
- current_state = CommandState::DRAWING_STROKE;
- current_stroke.points.clear();
- // Add first point, adjusted for layer offset
- current_stroke.points.push_back(ScreenToWorld(mousePos, layers[selected_layer_index].offset));
- } else if (current_state == CommandState::SPLITTING_END || current_state == CommandState::SPLITTING_BEGINNING) {
- // Confirm split
- if (split_count > 0 && !layers.empty() && !layers[selected_layer_index].strokes.empty() && split_count < layers[selected_layer_index].strokes.size()) {
- Layer new_layer;
- new_layer.offset = layers[selected_layer_index].offset; // Inherit offset for now
- auto& current_strokes = layers[selected_layer_index].strokes;
- if (current_state == CommandState::SPLITTING_END) {
- // Move last 'split_count' strokes
- auto start_it = current_strokes.end() - split_count;
- std::move(start_it, current_strokes.end(), std::back_inserter(new_layer.strokes));
- current_strokes.erase(start_it, current_strokes.end());
- // Insert new layer *after* current
- selected_layer_index++;
- layers.insert(layers.begin() + selected_layer_index, std::move(new_layer));
- } else { // SPLITTING_BEGINNING
- // Move first 'split_count' strokes
- auto end_it = current_strokes.begin() + split_count;
- std::move(current_strokes.begin(), end_it, std::back_inserter(new_layer.strokes));
- current_strokes.erase(current_strokes.begin(), end_it);
- // Insert new layer *before* current (index stays same, points to new layer)
- layers.insert(layers.begin() + selected_layer_index, std::move(new_layer));
- // No need to change selected_layer_index, it now points to the new layer
- }
- }
- // Reset split state regardless of success
- current_state = CommandState::IDLE;
- split_count = 0;
- }
- } else if (e.button.button == SDL_BUTTON_RIGHT) {
- rmb_down = true;
- if (current_state == CommandState::DRAWING_STROKE) {
- // Cancel drawing
- current_stroke.points.clear();
- // Don't reset state until LMB is also up
- } else if (current_state == CommandState::TRANSPOSING_LAYER) {
- // Cancel transpose
- layers[selected_layer_index].offset = original_layer_offset;
- current_state = CommandState::IDLE;
- mmb_down = false; // Ensure MMB state is reset
- } else if (current_state == CommandState::SPLITTING_END || current_state == CommandState::SPLITTING_BEGINNING) {
- // Cancel split
- current_state = CommandState::IDLE;
- split_count = 0;
- }
- } else if (e.button.button == SDL_BUTTON_MIDDLE) {
- if (current_state == CommandState::IDLE && !layers.empty()) {
- mmb_down = true;
- current_state = CommandState::TRANSPOSING_LAYER;
- transpose_start_pos = mousePos;
- original_layer_offset = layers[selected_layer_index].offset;
- }
- }
- } else if (e.type == SDL_MOUSEBUTTONUP) {
- SDL_Point mousePos = {e.button.x, e.button.y};
- if (e.button.button == SDL_BUTTON_LEFT) {
- lmb_down = false;
- if (current_state == CommandState::DRAWING_STROKE) {
- if (!current_stroke.points.empty() && !rmb_down) { // Only add if not cancelled
- // Add the final point
- current_stroke.points.push_back(ScreenToWorld(mousePos, layers[selected_layer_index].offset));
- if (current_stroke.points.size() >= 2) { // Need at least 2 points for a line
- layers[selected_layer_index].strokes.push_back(std::move(current_stroke));
- }
- }
- // Reset drawing state only if RMB is also up (or wasn't pressed)
- if (!rmb_down) {
- current_state = CommandState::IDLE;
- current_stroke.points.clear(); // Clear just in case
- }
- }
- // If RMB was down (cancel) and LMB is now up, reset state if needed
- else if (rmb_down && current_state == CommandState::DRAWING_STROKE) {
- current_state = CommandState::IDLE;
- current_stroke.points.clear();
- }
- } else if (e.button.button == SDL_BUTTON_RIGHT) {
- rmb_down = false;
- // If LMB was down (drawing) and RMB is now up, reset state if needed
- if (lmb_down && current_state == CommandState::DRAWING_STROKE) {
- current_state = CommandState::IDLE;
- current_stroke.points.clear();
- }
- } else if (e.button.button == SDL_BUTTON_MIDDLE) {
- mmb_down = false;
- if (current_state == CommandState::TRANSPOSING_LAYER) {
- current_state = CommandState::IDLE;
- // Offset is already updated during mouse motion
- }
- }
- }
- // --- Mouse Motion Event ---
- else if (e.type == SDL_MOUSEMOTION) {
- SDL_Point mousePos = {e.motion.x, e.motion.y};
- if (current_state == CommandState::DRAWING_STROKE && lmb_down && !rmb_down) {
- // Add point relative to layer offset
- SDL_Point worldPos = ScreenToWorld(mousePos, layers[selected_layer_index].offset);
- // Avoid adding duplicate points if the mouse hasn't moved significantly
- // (in world coordinates!)
- if (current_stroke.points.empty() ||
- current_stroke.points.back().x != worldPos.x ||
- current_stroke.points.back().y != worldPos.y)
- {
- current_stroke.points.push_back(worldPos);
- }
- } else if (current_state == CommandState::TRANSPOSING_LAYER && mmb_down) {
- int dx = mousePos.x - transpose_start_pos.x;
- int dy = mousePos.y - transpose_start_pos.y;
- layers[selected_layer_index].offset.x = original_layer_offset.x + dx;
- layers[selected_layer_index].offset.y = original_layer_offset.y + dy;
- }
- }
- // --- Mouse Wheel Event (for Splitting) ---
- else if (e.type == SDL_MOUSEWHEEL) {
- if (current_state == CommandState::SPLITTING_END || current_state == CommandState::SPLITTING_BEGINNING) {
- if (!layers.empty() && !layers[selected_layer_index].strokes.empty()) {
- int max_split = static_cast<int>(layers[selected_layer_index].strokes.size());
- if (e.wheel.y > 0) { // Scroll Up
- split_count++;
- } else if (e.wheel.y < 0) { // Scroll Down
- split_count--;
- }
- // Clamp the value. Don't be an idiot.
- split_count = std::max(0, std::min(split_count, max_split));
- }
- }
- }
- // --- Keyboard Events ---
- else if (e.type == SDL_KEYDOWN) {
- // Only process keydown if not in the middle of a mouse action
- if (current_state == CommandState::IDLE) {
- SDL_Keycode key = e.key.keysym.sym;
- SDL_Keymod mod = SDL_GetModState();
- bool shift_pressed = (mod & KMOD_SHIFT);
- if (key == SDLK_TAB && !layers.empty()) {
- if (shift_pressed) { // Shift + Tab: Previous Layer
- if (selected_layer_index == 0) {
- selected_layer_index = layers.size() - 1; // Wrap to last
- } else {
- selected_layer_index--;
- }
- } else { // Tab: Next Layer
- selected_layer_index++;
- if (selected_layer_index >= layers.size()) {
- selected_layer_index = 0; // Wrap to first
- }
- }
- } else if (key == SDLK_INSERT) {
- // Insert new layer *after* current
- size_t insert_pos = selected_layer_index + 1;
- layers.insert(layers.begin() + insert_pos, Layer());
- selected_layer_index = insert_pos; // Select the new layer
- } else if (key == SDLK_DELETE && !layers.empty()) {
- layers.erase(layers.begin() + selected_layer_index);
- if (layers.empty()) {
- // If we deleted the last layer, add a new default one
- layers.push_back({});
- selected_layer_index = 0;
- } else {
- // Select previous layer, or 0 if we deleted the first
- if (selected_layer_index >= layers.size()) {
- selected_layer_index = layers.size() - 1;
- }
- // No change needed if selected_layer_index is still valid
- }
- } else if (key == SDLK_s && !layers.empty()) {
- if (shift_pressed) { // Shift + S: Split Beginning
- current_state = CommandState::SPLITTING_BEGINNING;
- split_count = 0; // Reset selection
- } else { // S: Split End
- current_state = CommandState::SPLITTING_END;
- split_count = 0; // Reset selection
- }
- }
- }
- }
- } // End event polling loop
- // --- Rendering ---
- // Time to draw everything. Don't blink, you might miss it.
- // 1. Clear Screen
- // Use special background if the selected layer is empty
- bool selected_is_empty = layers.empty() || layers[selected_layer_index].strokes.empty();
- const SDL_Color& bg_color = (selected_is_empty && current_state == CommandState::IDLE) ? COLOR_BACKGROUND_SELECTED_EMPTY : COLOR_BACKGROUND;
- SetRenderDrawColor(renderer, bg_color);
- SDL_RenderClear(renderer);
- // 2. Draw Layers
- for (size_t i = 0; i < layers.size(); ++i) {
- const auto& layer = layers[i];
- bool is_selected_layer = (i == selected_layer_index);
- // Determine base color for strokes in this layer
- SDL_Color base_stroke_color = is_selected_layer ? COLOR_FOREGROUND_SELECTED : COLOR_FOREGROUND;
- if (current_state == CommandState::SPLITTING_END && is_selected_layer) {
- int num_strokes = static_cast<int>(layer.strokes.size());
- for (int j = 0; j < num_strokes; ++j) {
- // Last 'split_count' strokes are selected (red)
- bool is_selected_for_split = (j >= num_strokes - split_count);
- const SDL_Color& color = is_selected_for_split ? COLOR_SPLIT_SELECTED : COLOR_SPLIT_DULL;
- DrawStroke(renderer, layer.strokes[j], layer.offset, color);
- }
- } else if (current_state == CommandState::SPLITTING_BEGINNING && is_selected_layer) {
- int num_strokes = static_cast<int>(layer.strokes.size());
- for (int j = 0; j < num_strokes; ++j) {
- // First 'split_count' strokes are selected (red)
- bool is_selected_for_split = (j < split_count);
- const SDL_Color& color = is_selected_for_split ? COLOR_SPLIT_SELECTED : COLOR_SPLIT_DULL;
- DrawStroke(renderer, layer.strokes[j], layer.offset, color);
- }
- } else {
- // Regular rendering
- for (const auto& stroke : layer.strokes) {
- DrawStroke(renderer, stroke, layer.offset, base_stroke_color);
- }
- }
- }
- // 3. Draw the stroke currently being drawn (if any)
- if (current_state == CommandState::DRAWING_STROKE && !current_stroke.points.empty() && !rmb_down) {
- DrawStroke(renderer, current_stroke, layers[selected_layer_index].offset, COLOR_FOREGROUND_SELECTED);
- }
- // 4. Present Renderer
- SDL_RenderPresent(renderer);
- } // End main loop
- // --- Cleanup ---
- // You better not forget this part, or you'll have memory leaks! 💢
- SDL_DestroyRenderer(renderer);
- SDL_DestroyWindow(window);
- SDL_Quit();
- return 0; // Finished. Finally. Now leave me alone.
- }
- /*
- --- How to Build (Example using g++) ---
- You need SDL2 development libraries installed! How you do that depends on your OS.
- Google it yourself, baka!
- Example command (Linux/macOS):
- g++ your_source_file.cpp -o your_executable -lSDL2
- Example command (Windows with MinGW):
- g++ your_source_file.cpp -o your_executable.exe -Ipath/to/SDL2/include -Lpath/to/SDL2/lib -lmingw32 -lSDL2main -lSDL2
- Replace 'your_source_file.cpp' with the name you save this code as.
- Replace 'your_executable' with the desired output name.
- Replace 'path/to/SDL2/include' and 'path/to/SDL2/lib' with the actual paths on your system if needed.
- Honestly, setting up build environments is such a pain... why do I even bother? 😤
- */
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement