This commit is contained in:
2026-03-03 21:14:39 +03:00
parent bf8e3bad6f
commit ff28dbb2f5
9 changed files with 376 additions and 58 deletions
+1
View File
@@ -23,6 +23,7 @@ add_executable(SortLab
src/Sorter.cpp
src/UI.cpp
src/OperationsHistory.cpp
src/ProgressMap.cpp
src/sorters/BubbleSorter.cpp
src/sorters/SelectionSorter.cpp
src/sorters/InsertionSorter.cpp
+6 -3
View File
@@ -6,6 +6,7 @@
#include "Sorter.hpp"
#include "UI.hpp"
#include "OperationsHistory.hpp"
#include "ProgressMap.hpp"
class App {
public:
@@ -16,7 +17,7 @@ private:
void handleEvents();
void update(float dt);
void render();
void renderHistogram();
void renderDeltaHistogram();
void switchSorter(std::unique_ptr<Sorter> newSorter);
void generateBeepSound();
void playBeep(float pitch);
@@ -26,6 +27,7 @@ private:
std::unique_ptr<Sorter> currentSorter_;
UI ui_;
OperationsHistory opsHistory_;
ProgressMap progressMap_;
bool isPlaying_;
float timeSinceLastStep_;
int stepsPerFrame_;
@@ -41,6 +43,7 @@ private:
size_t lastComparisons_;
size_t lastSwaps_;
sf::Font histogramFont_;
bool histogramFontLoaded_;
sf::Font bottomPanelFont_;
bool bottomPanelFontLoaded_;
bool showInfo_;
};
+6 -5
View File
@@ -4,14 +4,15 @@
class OperationsHistory {
public:
OperationsHistory(size_t maxSamples = 300);
OperationsHistory(size_t maxSamples = 400);
void record(size_t comparisons);
void record(size_t currentComparisons);
void reset();
const std::deque<size_t>& getHistory() const;
size_t getMaxValue() const;
const std::deque<size_t>& getDeltaHistory() const;
size_t getMaxDelta() const;
private:
std::deque<size_t> compareHistory_;
std::deque<size_t> deltaHistory_;
size_t maxSamples_;
size_t lastComparisons_;
};
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <SFML/Graphics.hpp>
#include "Array.hpp"
class ProgressMap {
public:
ProgressMap();
void draw(sf::RenderWindow& window, const Array& array, sf::Font& font, bool fontLoaded);
private:
sf::Color getColorForState(Array::State state) const;
};
+1
View File
@@ -9,6 +9,7 @@ public:
void update(const Sorter& sorter, bool isPlaying, bool isFinished, int stepsPerFrame, const Array& array);
void draw(sf::RenderWindow& window);
void drawInfoOverlay(sf::RenderWindow& window);
private:
sf::Font font_;
+72 -38
View File
@@ -10,7 +10,8 @@ App::App()
, array_(100)
, currentSorter_(std::make_unique<BubbleSorter>())
, ui_()
, opsHistory_(300)
, opsHistory_(400)
, progressMap_()
, isPlaying_(false)
, timeSinceLastStep_(0.0f)
, stepsPerFrame_(1)
@@ -20,14 +21,15 @@ App::App()
, sweepDelay_(0.005f)
, lastComparisons_(0)
, lastSwaps_(0)
, histogramFontLoaded_(false) {
, bottomPanelFontLoaded_(false)
, showInfo_(false) {
window_.setFramerateLimit(60);
generateBeepSound();
beepSound_.setBuffer(beepBuffer_);
beepSound_.setVolume(100.0f);
if (histogramFont_.loadFromFile("assets/fonts/JetBrainsMono-Regular.ttf")) {
histogramFontLoaded_ = true;
if (bottomPanelFont_.loadFromFile("assets/fonts/JetBrainsMono-Regular.ttf")) {
bottomPanelFontLoaded_ = true;
}
}
@@ -109,6 +111,10 @@ void App::handleEvents() {
window_.close();
break;
case sf::Keyboard::I:
showInfo_ = !showInfo_;
break;
default:
break;
}
@@ -182,8 +188,11 @@ void App::update(float dt) {
void App::render() {
window_.clear(sf::Color(10, 12, 20));
const float histogramHeight = 120.0f;
const float availableHeight = window_.getSize().y - histogramHeight;
const float topUIHeight = 80.0f;
const float bottomPanelHeight = 140.0f;
const float mainAreaTop = topUIHeight;
const float mainAreaBottom = window_.getSize().y - bottomPanelHeight;
const float mainAreaHeight = mainAreaBottom - mainAreaTop;
float width = window_.getSize().x / static_cast<float>(array_.getSize());
float maxVal = static_cast<float>(array_.getSize());
@@ -191,9 +200,9 @@ void App::render() {
sf::VertexArray vertices(sf::Quads);
for (int i = 0; i < array_.getSize(); ++i) {
float height = (array_.getValue(i) / maxVal) * (availableHeight * 0.75f);
float height = (array_.getValue(i) / maxVal) * (mainAreaHeight * 0.9f);
float x = i * width;
float y = availableHeight - height;
float y = mainAreaBottom - height;
sf::Color barColor;
bool useGradient = false;
@@ -230,51 +239,71 @@ void App::render() {
static_cast<sf::Uint8>(bottomColor.b + (topColor.b - bottomColor.b) * normalizedValue * 0.6f)
);
vertices.append(sf::Vertex(sf::Vector2f(x, availableHeight), gradientBottom));
vertices.append(sf::Vertex(sf::Vector2f(x + width - 1.0f, availableHeight), gradientBottom));
vertices.append(sf::Vertex(sf::Vector2f(x, mainAreaBottom), gradientBottom));
vertices.append(sf::Vertex(sf::Vector2f(x + width - 1.0f, mainAreaBottom), gradientBottom));
vertices.append(sf::Vertex(sf::Vector2f(x + width - 1.0f, y), gradientTop));
vertices.append(sf::Vertex(sf::Vector2f(x, y), gradientTop));
} else {
vertices.append(sf::Vertex(sf::Vector2f(x, availableHeight), barColor));
vertices.append(sf::Vertex(sf::Vector2f(x + width - 1.0f, availableHeight), barColor));
vertices.append(sf::Vertex(sf::Vector2f(x, mainAreaBottom), barColor));
vertices.append(sf::Vertex(sf::Vector2f(x + width - 1.0f, mainAreaBottom), barColor));
vertices.append(sf::Vertex(sf::Vector2f(x + width - 1.0f, y), barColor));
vertices.append(sf::Vertex(sf::Vector2f(x, y), barColor));
}
}
window_.draw(vertices);
renderHistogram();
sf::RectangleShape separator(sf::Vector2f(window_.getSize().x, 1.0f));
separator.setPosition(0.0f, mainAreaBottom);
separator.setFillColor(sf::Color(40, 45, 60));
window_.draw(separator);
renderDeltaHistogram();
progressMap_.draw(window_, array_, bottomPanelFont_, bottomPanelFontLoaded_);
sf::RectangleShape verticalSeparator(sf::Vector2f(1.0f, bottomPanelHeight));
verticalSeparator.setPosition(1066.0f, mainAreaBottom);
verticalSeparator.setFillColor(sf::Color(40, 45, 60));
window_.draw(verticalSeparator);
ui_.draw(window_);
if (showInfo_) {
ui_.drawInfoOverlay(window_);
}
window_.display();
}
void App::renderHistogram() {
const float histogramHeight = 120.0f;
const float histogramY = window_.getSize().y - histogramHeight;
void App::renderDeltaHistogram() {
const float startX = 0.0f;
const float startY = 760.0f;
const float width = 1066.0f;
const float height = 140.0f;
sf::RectangleShape background(sf::Vector2f(window_.getSize().x, histogramHeight));
background.setPosition(0.0f, histogramY);
background.setFillColor(sf::Color(15, 18, 30));
sf::RectangleShape background(sf::Vector2f(width, height));
background.setPosition(startX, startY);
background.setFillColor(sf::Color(12, 15, 25));
window_.draw(background);
const auto& history = opsHistory_.getHistory();
const auto& deltaHistory = opsHistory_.getDeltaHistory();
if (!history.empty()) {
size_t maxValue = opsHistory_.getMaxValue();
if (maxValue == 0) maxValue = 1;
if (!deltaHistory.empty()) {
size_t maxDelta = opsHistory_.getMaxDelta();
if (maxDelta == 0) maxDelta = 1;
float barWidth = window_.getSize().x / 300.0f;
float barWidth = width / 400.0f;
float availableHeight = height - 25.0f;
sf::VertexArray histogramBars(sf::Quads);
for (size_t i = 0; i < history.size(); ++i) {
float barHeight = (static_cast<float>(history[i]) / static_cast<float>(maxValue)) * (histogramHeight - 30.0f);
float x = i * barWidth;
float y = histogramY + histogramHeight - barHeight - 5.0f;
for (size_t i = 0; i < deltaHistory.size(); ++i) {
float barHeight = (static_cast<float>(deltaHistory[i]) / static_cast<float>(maxDelta)) * availableHeight;
float x = startX + i * barWidth;
float y = startY + height - barHeight;
float normalizedValue = static_cast<float>(history[i]) / static_cast<float>(maxValue);
sf::Color lowColor(0, 120, 255);
sf::Color highColor(0, 220, 255);
float normalizedValue = static_cast<float>(deltaHistory[i]) / static_cast<float>(maxDelta);
sf::Color lowColor(0, 80, 200);
sf::Color highColor(0, 240, 255);
sf::Color barColor(
static_cast<sf::Uint8>(lowColor.r + (highColor.r - lowColor.r) * normalizedValue),
@@ -282,8 +311,8 @@ void App::renderHistogram() {
static_cast<sf::Uint8>(lowColor.b + (highColor.b - lowColor.b) * normalizedValue)
);
histogramBars.append(sf::Vertex(sf::Vector2f(x, histogramY + histogramHeight - 5.0f), barColor));
histogramBars.append(sf::Vertex(sf::Vector2f(x + barWidth, histogramY + histogramHeight - 5.0f), barColor));
histogramBars.append(sf::Vertex(sf::Vector2f(x, startY + height), barColor));
histogramBars.append(sf::Vertex(sf::Vector2f(x + barWidth, startY + height), barColor));
histogramBars.append(sf::Vertex(sf::Vector2f(x + barWidth, y), barColor));
histogramBars.append(sf::Vertex(sf::Vector2f(x, y), barColor));
}
@@ -291,13 +320,18 @@ void App::renderHistogram() {
window_.draw(histogramBars);
}
if (histogramFontLoaded_) {
sf::RectangleShape baseline(sf::Vector2f(width, 1.0f));
baseline.setPosition(startX, startY + height - 1.0f);
baseline.setFillColor(sf::Color(60, 70, 90));
window_.draw(baseline);
if (bottomPanelFontLoaded_) {
sf::Text label;
label.setFont(histogramFont_);
label.setString("Comparisons over time");
label.setCharacterSize(14);
label.setFont(bottomPanelFont_);
label.setString("Delta Comparisons / step");
label.setCharacterSize(13);
label.setFillColor(sf::Color(180, 180, 180));
label.setPosition(10.0f, histogramY + 5.0f);
label.setPosition(startX + 10.0f, startY + 5.0f);
window_.draw(label);
}
}
+18 -11
View File
@@ -2,29 +2,36 @@
#include <algorithm>
OperationsHistory::OperationsHistory(size_t maxSamples)
: maxSamples_(maxSamples) {
: maxSamples_(maxSamples), lastComparisons_(0) {
}
void OperationsHistory::record(size_t comparisons) {
compareHistory_.push_back(comparisons);
void OperationsHistory::record(size_t currentComparisons) {
size_t delta = 0;
if (currentComparisons >= lastComparisons_) {
delta = currentComparisons - lastComparisons_;
}
if (compareHistory_.size() > maxSamples_) {
compareHistory_.pop_front();
deltaHistory_.push_back(delta);
lastComparisons_ = currentComparisons;
if (deltaHistory_.size() > maxSamples_) {
deltaHistory_.pop_front();
}
}
void OperationsHistory::reset() {
compareHistory_.clear();
deltaHistory_.clear();
lastComparisons_ = 0;
}
const std::deque<size_t>& OperationsHistory::getHistory() const {
return compareHistory_;
const std::deque<size_t>& OperationsHistory::getDeltaHistory() const {
return deltaHistory_;
}
size_t OperationsHistory::getMaxValue() const {
if (compareHistory_.empty()) {
size_t OperationsHistory::getMaxDelta() const {
if (deltaHistory_.empty()) {
return 1;
}
return *std::max_element(compareHistory_.begin(), compareHistory_.end());
return *std::max_element(deltaHistory_.begin(), deltaHistory_.end());
}
+82
View File
@@ -0,0 +1,82 @@
#include "ProgressMap.hpp"
#include <cmath>
ProgressMap::ProgressMap() {
}
void ProgressMap::draw(sf::RenderWindow& window, const Array& array, sf::Font& font, bool fontLoaded) {
const float startX = 1066.0f;
const float startY = 760.0f;
const float width = 534.0f;
const float height = 140.0f;
sf::RectangleShape background(sf::Vector2f(width, height));
background.setPosition(startX, startY);
background.setFillColor(sf::Color(12, 15, 25));
window.draw(background);
if (fontLoaded) {
sf::Text label;
label.setFont(font);
label.setString("Progress Map");
label.setCharacterSize(13);
label.setFillColor(sf::Color(180, 180, 180));
label.setPosition(startX + 10.0f, startY + 5.0f);
window.draw(label);
}
int arraySize = array.getSize();
int cols = static_cast<int>(std::sqrt(arraySize));
int rows = (arraySize + cols - 1) / cols;
float availableWidth = width - 20.0f;
float availableHeight = height - 30.0f;
float squareSizeW = availableWidth / static_cast<float>(cols);
float squareSizeH = availableHeight / static_cast<float>(rows);
float squareSize = std::min(squareSizeW, squareSizeH);
if (squareSize < 1.0f) squareSize = 1.0f;
float gridWidth = cols * squareSize;
float gridHeight = rows * squareSize;
float offsetX = startX + (width - gridWidth) * 0.5f;
float offsetY = startY + 30.0f + (availableHeight - gridHeight) * 0.5f;
sf::VertexArray squares(sf::Quads);
for (int i = 0; i < arraySize; ++i) {
int col = i % cols;
int row = i / cols;
float x = offsetX + col * squareSize;
float y = offsetY + row * squareSize;
sf::Color color = getColorForState(array.getState(i));
float gap = 1.0f;
float actualSize = squareSize - gap;
squares.append(sf::Vertex(sf::Vector2f(x, y), color));
squares.append(sf::Vertex(sf::Vector2f(x + actualSize, y), color));
squares.append(sf::Vertex(sf::Vector2f(x + actualSize, y + actualSize), color));
squares.append(sf::Vertex(sf::Vector2f(x, y + actualSize), color));
}
window.draw(squares);
}
sf::Color ProgressMap::getColorForState(Array::State state) const {
switch (state) {
case Array::State::NORMAL:
return sf::Color(100, 120, 150);
case Array::State::COMPARE:
return sf::Color(0, 220, 255);
case Array::State::SWAP:
return sf::Color(255, 120, 30);
case Array::State::SORTED:
return sf::Color(50, 220, 120);
default:
return sf::Color(100, 120, 150);
}
}
+177 -1
View File
@@ -1,3 +1,4 @@
#pragma execution_character_set("utf-8")
#include "UI.hpp"
#include <iostream>
@@ -86,7 +87,25 @@ void UI::draw(sf::RenderWindow& window) {
return;
}
float leftWidth = 350.0f;
float maxWidth = 0.0f;
sf::FloatRect algoBounds = algorithmText_.getLocalBounds();
sf::FloatRect stateBounds = stateText_.getLocalBounds();
sf::FloatRect timeBounds = timeComplexityText_.getLocalBounds();
sf::FloatRect spaceBounds = spaceComplexityText_.getLocalBounds();
sf::FloatRect compBounds = comparisonsText_.getLocalBounds();
sf::FloatRect swapBounds = swapsText_.getLocalBounds();
sf::FloatRect speedBounds = speedText_.getLocalBounds();
maxWidth = std::max(maxWidth, algoBounds.width);
maxWidth = std::max(maxWidth, stateBounds.width);
maxWidth = std::max(maxWidth, timeBounds.width);
maxWidth = std::max(maxWidth, spaceBounds.width);
maxWidth = std::max(maxWidth, compBounds.width);
maxWidth = std::max(maxWidth, swapBounds.width);
maxWidth = std::max(maxWidth, speedBounds.width);
float leftWidth = maxWidth + 24.0f;
float leftHeight = 215.0f;
leftBackground_.setSize(sf::Vector2f(leftWidth, leftHeight));
leftBackground_.setPosition(5.0f, 5.0f);
@@ -113,3 +132,160 @@ void UI::draw(sf::RenderWindow& window) {
window.draw(speedText_);
window.draw(controlsText_);
}
void UI::drawInfoOverlay(sf::RenderWindow& window) {
if (!fontLoaded_) {
return;
}
auto toSfStr = [](const char* utf8) -> sf::String {
std::string s(utf8);
return sf::String::fromUtf8(s.begin(), s.end());
};
float windowWidth = static_cast<float>(window.getSize().x);
float windowHeight = static_cast<float>(window.getSize().y);
sf::RectangleShape overlay(sf::Vector2f(windowWidth, windowHeight));
overlay.setPosition(0.0f, 0.0f);
overlay.setFillColor(sf::Color(8, 10, 18, 230));
window.draw(overlay);
sf::Text title;
title.setFont(font_);
title.setCharacterSize(36);
title.setFillColor(sf::Color::White);
title.setString(toSfStr(u8"SortLab — визуализатор алгоритмов сортировки"));
sf::Text subtitle;
subtitle.setFont(font_);
subtitle.setCharacterSize(18);
subtitle.setFillColor(sf::Color(160, 160, 160));
subtitle.setString(toSfStr(u8"Учебный инструмент для наглядного изучения алгоритмов сортировки"));
sf::Text section1;
section1.setFont(font_);
section1.setCharacterSize(20);
section1.setFillColor(sf::Color(0, 220, 255));
section1.setString(toSfStr(u8"О программе"));
sf::Text body1;
body1.setFont(font_);
body1.setCharacterSize(16);
body1.setFillColor(sf::Color(200, 200, 200));
body1.setString(toSfStr(u8"SortLab визуализирует работу алгоритмов сортировки в реальном времени.\nКаждый столбик — это элемент массива. Высота = значение элемента.\nЦвета: серый — обычный, голубой — сравнение, оранжевый — перестановка, зелёный — готово."));
sf::Text section2;
section2.setFont(font_);
section2.setCharacterSize(20);
section2.setFillColor(sf::Color(0, 220, 255));
section2.setString(toSfStr(u8"Алгоритмы и сложность"));
sf::Text body2;
body2.setFont(font_);
body2.setCharacterSize(16);
body2.setFillColor(sf::Color(200, 200, 200));
body2.setString(toSfStr(u8"[1] Bubble Sort Время: O(n²) / O(n²) / O(n²) Память: O(1)\n[2] Selection Sort Время: O(n²) / O(n²) / O(n²) Память: O(1)\n[3] Insertion Sort Время: O(n) / O(n²) / O(n²) Память: O(1)\n[4] Merge Sort Время: O(n log n) / O(n log n) / ... Память: O(n)\n[5] Quick Sort Время: O(n log n) / O(n log n) / ... Память: O(log n)"));
sf::Text section3;
section3.setFont(font_);
section3.setCharacterSize(20);
section3.setFillColor(sf::Color(0, 220, 255));
section3.setString(toSfStr(u8"Δ Гистограмма (внизу слева)"));
sf::Text body3;
body3.setFont(font_);
body3.setCharacterSize(16);
body3.setFillColor(sf::Color(200, 200, 200));
body3.setString(toSfStr(u8"Показывает количество сравнений на каждом шаге сортировки.\nВысокий пик = много сравнений за один шаг.\nBubble Sort даёт ровную линию, Quick Sort — резкие пики в начале."));
sf::Text section4;
section4.setFont(font_);
section4.setCharacterSize(20);
section4.setFillColor(sf::Color(0, 220, 255));
section4.setString(toSfStr(u8"Карта прогресса (внизу справа)"));
sf::Text body4;
body4.setFont(font_);
body4.setCharacterSize(16);
body4.setFillColor(sf::Color(200, 200, 200));
body4.setString(toSfStr(u8"Миниатюрная копия всего массива в виде сетки цветных квадратиков.\nПозволяет видеть глобальный прогресс сортировки даже при большом массиве.\nЗелёные квадраты = отсортированные элементы."));
sf::Text footer;
footer.setFont(font_);
footer.setCharacterSize(15);
footer.setFillColor(sf::Color(100, 100, 100));
footer.setString(toSfStr(u8"Нажми [I] чтобы закрыть | [Q] выход | [Space] старт/пауза | [R] перемешать"));
float totalHeight = 0.0f;
totalHeight += title.getLocalBounds().height + 10.0f;
totalHeight += subtitle.getLocalBounds().height + 30.0f;
totalHeight += section1.getLocalBounds().height + 6.0f;
totalHeight += body1.getLocalBounds().height + 24.0f;
totalHeight += section2.getLocalBounds().height + 6.0f;
totalHeight += body2.getLocalBounds().height + 24.0f;
totalHeight += section3.getLocalBounds().height + 6.0f;
totalHeight += body3.getLocalBounds().height + 24.0f;
totalHeight += section4.getLocalBounds().height + 6.0f;
totalHeight += body4.getLocalBounds().height + 30.0f;
totalHeight += footer.getLocalBounds().height;
float startY = (windowHeight - totalHeight) * 0.5f;
if (startY < 20.0f) startY = 20.0f;
float currentY = startY;
sf::FloatRect titleBounds = title.getLocalBounds();
title.setPosition((windowWidth - titleBounds.width) * 0.5f, currentY);
window.draw(title);
currentY += titleBounds.height + 10.0f;
sf::FloatRect subtitleBounds = subtitle.getLocalBounds();
subtitle.setPosition((windowWidth - subtitleBounds.width) * 0.5f, currentY);
window.draw(subtitle);
currentY += subtitleBounds.height + 30.0f;
sf::FloatRect section1Bounds = section1.getLocalBounds();
section1.setPosition((windowWidth - section1Bounds.width) * 0.5f, currentY);
window.draw(section1);
currentY += section1Bounds.height + 6.0f;
sf::FloatRect body1Bounds = body1.getLocalBounds();
body1.setPosition((windowWidth - body1Bounds.width) * 0.5f, currentY);
window.draw(body1);
currentY += body1Bounds.height + 24.0f;
sf::FloatRect section2Bounds = section2.getLocalBounds();
section2.setPosition((windowWidth - section2Bounds.width) * 0.5f, currentY);
window.draw(section2);
currentY += section2Bounds.height + 6.0f;
sf::FloatRect body2Bounds = body2.getLocalBounds();
body2.setPosition((windowWidth - body2Bounds.width) * 0.5f, currentY);
window.draw(body2);
currentY += body2Bounds.height + 24.0f;
sf::FloatRect section3Bounds = section3.getLocalBounds();
section3.setPosition((windowWidth - section3Bounds.width) * 0.5f, currentY);
window.draw(section3);
currentY += section3Bounds.height + 6.0f;
sf::FloatRect body3Bounds = body3.getLocalBounds();
body3.setPosition((windowWidth - body3Bounds.width) * 0.5f, currentY);
window.draw(body3);
currentY += body3Bounds.height + 24.0f;
sf::FloatRect section4Bounds = section4.getLocalBounds();
section4.setPosition((windowWidth - section4Bounds.width) * 0.5f, currentY);
window.draw(section4);
currentY += section4Bounds.height + 6.0f;
sf::FloatRect body4Bounds = body4.getLocalBounds();
body4.setPosition((windowWidth - body4Bounds.width) * 0.5f, currentY);
window.draw(body4);
currentY += body4Bounds.height + 30.0f;
sf::FloatRect footerBounds = footer.getLocalBounds();
footer.setPosition((windowWidth - footerBounds.width) * 0.5f, currentY);
window.draw(footer);
}