From bc65dbcd227cc4beeedd8b9b04f063db0bfb9feb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:57:25 +0000 Subject: [PATCH 1/9] Initial plan From 3cda5bacf6df5486b7f153e9c67fbe4095a45fdc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 03:04:07 +0000 Subject: [PATCH 2/9] Add demo template selection feature with 31 demos Co-authored-by: wysaid <1430725+wysaid@users.noreply.github.com> --- cmake_template/ege_demos/camera_base.cpp | 441 +++++ cmake_template/ege_demos/camera_wave.cpp | 936 +++++++++++ cmake_template/ege_demos/game_gomoku.cpp | 737 +++++++++ cmake_template/ege_demos/game_snake.cpp | 91 ++ cmake_template/ege_demos/game_tetris.cpp | 395 +++++ cmake_template/ege_demos/game_type.cpp | 82 + cmake_template/ege_demos/getimage.jpg | Bin 0 -> 3680 bytes cmake_template/ege_demos/getimage.png | Bin 0 -> 11001 bytes cmake_template/ege_demos/graph_5star.cpp | 54 + cmake_template/ege_demos/graph_alpha.cpp | 46 + cmake_template/ege_demos/graph_arrow.cpp | 42 + .../ege_demos/graph_astar_pathfinding.cpp | 959 +++++++++++ cmake_template/ege_demos/graph_ball.cpp | 171 ++ cmake_template/ege_demos/graph_boids.cpp | 784 +++++++++ cmake_template/ege_demos/graph_catharine.cpp | 107 ++ cmake_template/ege_demos/graph_clock.cpp | 122 ++ .../graph_function_visualization.cpp | 639 ++++++++ .../ege_demos/graph_game_of_life.cpp | 749 +++++++++ cmake_template/ege_demos/graph_getimage.cpp | 64 + cmake_template/ege_demos/graph_julia.cpp | 635 ++++++++ cmake_template/ege_demos/graph_kmeans.cpp | 721 +++++++++ cmake_template/ege_demos/graph_lines.cpp | 258 +++ cmake_template/ege_demos/graph_mandelbrot.cpp | 222 +++ cmake_template/ege_demos/graph_mouseball.cpp | 207 +++ .../ege_demos/graph_new_drawimage.cpp | 86 + .../ege_demos/graph_rotateimage.cpp | 25 + .../ege_demos/graph_rotatetransparent.cpp | 32 + .../ege_demos/graph_sort_visualization.cpp | 1430 +++++++++++++++++ cmake_template/ege_demos/graph_star.cpp | 126 ++ cmake_template/ege_demos/graph_triangle.cpp | 169 ++ cmake_template/ege_demos/graph_wave_net.cpp | 311 ++++ src/demoOptions.ts | 334 ++++ src/extension.ts | 4 +- src/setupProject.ts | 99 +- 34 files changed, 11072 insertions(+), 6 deletions(-) create mode 100644 cmake_template/ege_demos/camera_base.cpp create mode 100644 cmake_template/ege_demos/camera_wave.cpp create mode 100644 cmake_template/ege_demos/game_gomoku.cpp create mode 100644 cmake_template/ege_demos/game_snake.cpp create mode 100644 cmake_template/ege_demos/game_tetris.cpp create mode 100644 cmake_template/ege_demos/game_type.cpp create mode 100644 cmake_template/ege_demos/getimage.jpg create mode 100644 cmake_template/ege_demos/getimage.png create mode 100644 cmake_template/ege_demos/graph_5star.cpp create mode 100644 cmake_template/ege_demos/graph_alpha.cpp create mode 100644 cmake_template/ege_demos/graph_arrow.cpp create mode 100644 cmake_template/ege_demos/graph_astar_pathfinding.cpp create mode 100644 cmake_template/ege_demos/graph_ball.cpp create mode 100644 cmake_template/ege_demos/graph_boids.cpp create mode 100644 cmake_template/ege_demos/graph_catharine.cpp create mode 100644 cmake_template/ege_demos/graph_clock.cpp create mode 100644 cmake_template/ege_demos/graph_function_visualization.cpp create mode 100644 cmake_template/ege_demos/graph_game_of_life.cpp create mode 100644 cmake_template/ege_demos/graph_getimage.cpp create mode 100644 cmake_template/ege_demos/graph_julia.cpp create mode 100644 cmake_template/ege_demos/graph_kmeans.cpp create mode 100644 cmake_template/ege_demos/graph_lines.cpp create mode 100644 cmake_template/ege_demos/graph_mandelbrot.cpp create mode 100644 cmake_template/ege_demos/graph_mouseball.cpp create mode 100644 cmake_template/ege_demos/graph_new_drawimage.cpp create mode 100644 cmake_template/ege_demos/graph_rotateimage.cpp create mode 100644 cmake_template/ege_demos/graph_rotatetransparent.cpp create mode 100644 cmake_template/ege_demos/graph_sort_visualization.cpp create mode 100644 cmake_template/ege_demos/graph_star.cpp create mode 100644 cmake_template/ege_demos/graph_triangle.cpp create mode 100644 cmake_template/ege_demos/graph_wave_net.cpp create mode 100644 src/demoOptions.ts diff --git a/cmake_template/ege_demos/camera_base.cpp b/cmake_template/ege_demos/camera_base.cpp new file mode 100644 index 0000000..ad4b704 --- /dev/null +++ b/cmake_template/ege_demos/camera_base.cpp @@ -0,0 +1,441 @@ +/** + * @file camera_base.cpp + * @author wysaid (this@xege.org) + * @brief 一个使用 ege 内置的相机模块打开相机的例子 + * @date 2025-05-18 + * + */ + +#define SHOW_CONSOLE 1 + +#ifndef NOMINMAX +#define NOMINMAX 1 +#endif + +#include + +#include +#include + +#include +#include +#include +#include + +// 文本本地化宏定义 +#ifdef _MSC_VER +// MSVC编译器使用中文文案 +#define TEXT_WINDOW_TITLE "EGE 相机演示" +#define TEXT_ERROR_NO_CAMERA "此演示需要相机设备才能运行。\n请连接相机设备后重试。" +#define TEXT_ERROR_NO_CAMERA_FEATURE \ + "当前 ege 版本不支持相机功能,请使用支持 C++17 的编译器。 如果是 MSVC 编译器,请使用 MSVC 2022 或更高版本。" +#define TEXT_ERROR_EXIT_HINT "按任意键退出。" +#define TEXT_ERROR_NO_DEVICE "未找到相机设备!" +#define TEXT_ERROR_OPEN_FAILED "打开相机设备失败!" +#define TEXT_ERROR_GRAB_FAILED "获取帧数据失败!" +#define TEXT_CAMERA_CLOSED "相机设备已关闭!" +#define TEXT_CAMERA_DEVICE "相机设备: %s" +#define TEXT_CPP11_REQUIRED "需要 C++11 或更高版本。" +#define TEXT_CAMERA_LIST_TITLE "可用相机设备:" +#define TEXT_CAMERA_LIST_ITEM " [%d] %s" +#define TEXT_CAMERA_SWITCH "按空格键切换相机,或按数字键选择 | 当前: [%d] %s" +#define TEXT_SWITCHING_CAMERA "正在切换到相机 %d..." +#define TEXT_RESOLUTION_LIST_TITLE "支持的分辨率:" +#define TEXT_RESOLUTION_ITEM " %dx%d" +#define TEXT_RESOLUTION_CURRENT " <-当前" +#define TEXT_RESOLUTION_SWITCH "按 ↑/↓ 箭头键切换分辨率" +#define TEXT_SWITCHING_RESOLUTION "正在切换到分辨率 %dx%d..." +#define TEXT_WINDOW_RESIZED "窗口大小已调整为 %dx%d" +#else +// 非MSVC编译器使用英文文案 +#define TEXT_WINDOW_TITLE "EGE Camera Demo" +#define TEXT_ERROR_NO_CAMERA "This demo requires a camera device to run.\nPlease connect a camera and try again." +#define TEXT_ERROR_NO_CAMERA_FEATURE \ + "The current version of EGE does not support camera features. Please use a compiler that supports C++17. If using MSVC, please use MSVC 2022 or later." +#define TEXT_ERROR_EXIT_HINT "Press any key to exit." +#define TEXT_ERROR_NO_DEVICE "No camera device found!!" +#define TEXT_ERROR_OPEN_FAILED "Failed to open camera device!!" +#define TEXT_ERROR_GRAB_FAILED "Failed to grab frame!!" +#define TEXT_CAMERA_CLOSED "Camera device closed!!" +#define TEXT_CAMERA_DEVICE "Camera device: %s" +#define TEXT_CPP11_REQUIRED "C++11 or higher is required." +#define TEXT_CAMERA_LIST_TITLE "Available cameras:" +#define TEXT_CAMERA_LIST_ITEM " [%d] %s" +#define TEXT_CAMERA_SWITCH "Press SPACE to switch camera, or press number key | Current: [%d] %s" +#define TEXT_SWITCHING_CAMERA "Switching to camera %d..." +#define TEXT_RESOLUTION_LIST_TITLE "Supported resolutions:" +#define TEXT_RESOLUTION_ITEM " %dx%d" +#define TEXT_RESOLUTION_CURRENT " <-Current" +#define TEXT_RESOLUTION_SWITCH "Press UP/DOWN arrow to switch resolution" +#define TEXT_SWITCHING_RESOLUTION "Switching to resolution %dx%d..." +#define TEXT_WINDOW_RESIZED "Window resized to %dx%d" +#endif + +// 判断一下 C++ 版本, 低于 C++11 的编译器不支持 +#if __cplusplus < 201103L +#pragma message("C++11 or higher is required.") + +int main() +{ + fputs(TEXT_CPP11_REQUIRED, stderr); + return 0; +} +#else + +#define WINDOW_WIDTH 1280 +#define WINDOW_HEIGHT 720 + +// 窗口尺寸限制 +#define MIN_LONG_EDGE 640 // 窗口长边最小值 +#define MAX_LONG_EDGE 1920 // 窗口长边最大值 + +// 根据相机分辨率调整窗口大小,保持比例一致 +// 返回 true 表示窗口大小发生了变化 +bool adjustWindowToCamera(int cameraWidth, int cameraHeight) +{ + int windowWidth = getwidth(); + int windowHeight = getheight(); + + // 计算相机的长边和短边 + int cameraLongEdge = (std::max)(cameraWidth, cameraHeight); + int cameraShortEdge = (std::min)(cameraWidth, cameraHeight); + float cameraRatio = (float)cameraWidth / cameraHeight; + + // 获取屏幕可用区域(考虑任务栏) + RECT workArea; + SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0); + int screenAvailWidth = workArea.right - workArea.left; + int screenAvailHeight = workArea.bottom - workArea.top; + // 留一点边距,避免窗口贴边 + screenAvailWidth -= 20; + screenAvailHeight -= 40; + + // 计算目标窗口长边 + int targetLongEdge = cameraLongEdge; + + // 限制1:长边不能小于 MIN_LONG_EDGE + if (targetLongEdge < MIN_LONG_EDGE) { + targetLongEdge = MIN_LONG_EDGE; + } + + // 限制2:长边不能大于 MAX_LONG_EDGE + if (targetLongEdge > MAX_LONG_EDGE) { + targetLongEdge = MAX_LONG_EDGE; + } + + // 根据长边和比例计算窗口尺寸 + int newWidth, newHeight; + if (cameraWidth >= cameraHeight) { + // 横向视频,宽度是长边 + newWidth = targetLongEdge; + newHeight = (int)(newWidth / cameraRatio); + } else { + // 纵向视频,高度是长边 + newHeight = targetLongEdge; + newWidth = (int)(newHeight * cameraRatio); + } + + // 限制3:不能超过屏幕可用区域 + if (newWidth > screenAvailWidth) { + float scale = (float)screenAvailWidth / newWidth; + newWidth = screenAvailWidth; + newHeight = (int)(newHeight * scale); + } + if (newHeight > screenAvailHeight) { + float scale = (float)screenAvailHeight / newHeight; + newHeight = screenAvailHeight; + newWidth = (int)(newWidth * scale); + } + + // 如果新尺寸与当前尺寸相同,无需调整 + if (newWidth == windowWidth && newHeight == windowHeight) { + return false; + } + + // 调用 initgraph 调整窗口大小 (无需 closegraph) + initgraph(newWidth, newHeight, INIT_RENDERMANUAL); + setcaption(TEXT_WINDOW_TITLE); + setbkmode(TRANSPARENT); + + printf(TEXT_WINDOW_RESIZED, newWidth, newHeight); + printf("\n"); + + return true; +} + +void showErrorWindow() +{ + settarget(nullptr); + setbkcolor(BLACK); + cleardevice(); + setcolor(RED); + if (hasCameraCaptureModule()) { + outtextrect(0, 0, getwidth(), getheight(), TEXT_ERROR_NO_CAMERA); + } else { + outtextrect(0, 0, getwidth(), getheight(), TEXT_ERROR_NO_CAMERA_FEATURE); + } + outtextxy(10, 30, TEXT_ERROR_EXIT_HINT); + getch(); + closegraph(); +} + +// 切换相机设备的辅助函数 +bool switchCamera(ege::CameraCapture& camera, int deviceIndex, int deviceCount, int resWidth = WINDOW_WIDTH, int resHeight = WINDOW_HEIGHT) +{ + if (deviceIndex < 0 || deviceIndex >= deviceCount) { + return false; + } + + // 先关闭当前相机 + if (camera.isStarted()) { + camera.close(); + } + + // 设置相机分辨率 + camera.setFrameSize(resWidth, resHeight); + camera.setFrameRate(30); + + // 打开新相机 + if (!camera.open(deviceIndex)) { + return false; + } + + camera.start(); + return true; +} + +// 分辨率信息结构体 +struct ResolutionItem +{ + int width; + int height; +}; + +// 获取并保存分辨率列表 +std::vector getResolutionList(ege::CameraCapture& camera) +{ + std::vector resolutions; + auto resList = camera.getDeviceSupportedResolutions(); + if (resList.count > 0) { + for (int i = 0; i < resList.count; ++i) { + resolutions.push_back({resList.info[i].width, resList.info[i].height}); + } + } + return resolutions; +} + +// 根据当前帧分辨率找到对应的分辨率索引 +int findCurrentResolutionIndex(const std::vector& resolutions, int width, int height) +{ + for (size_t i = 0; i < resolutions.size(); ++i) { + if (resolutions[i].width == width && resolutions[i].height == height) { + return static_cast(i); + } + } + return 0; // 如果找不到,返回第一个 +} + +int main() +{ + /// 在相机的高吞吐场景下, 不设置 RENDERMANUAL 会出现闪屏. + initgraph(1280, 720, INIT_RENDERMANUAL); + setcaption(TEXT_WINDOW_TITLE); + setbkmode(TRANSPARENT); + + ege::CameraCapture camera; + char msgBuffer[1024]; + + // 0: 不输出日志, 1: 输出警告日志, 2: 输出常规信息, 3: 输出调试信息, 超过 3 等同于 3. + ege::enableCameraModuleLog(2); + + // 保存设备信息 + std::vector deviceNames; + int deviceCount = 0; + int currentDeviceIndex = 0; + + // 分辨率相关 + std::vector resolutions; + int currentResolutionIndex = 0; + + { /// 获取所有相机设备的名称 + auto devices = camera.findDeviceNames(); + + if (devices.count == 0) { + fputs(TEXT_ERROR_NO_DEVICE, stderr); + showErrorWindow(); + return -1; + } + + deviceCount = devices.count; + for (int i = 0; i < devices.count; ++i) { + deviceNames.push_back(devices.info[i].name); + printf(TEXT_CAMERA_DEVICE, devices.info[i].name); + printf("\n"); + } + } + + // 打开第一个相机设备 + if (!switchCamera(camera, 0, deviceCount)) { + fputs(TEXT_ERROR_OPEN_FAILED, stderr); + return -1; + } + + // 获取分辨率列表 + resolutions = getResolutionList(camera); + + for (; camera.isStarted() && is_run(); delay_fps(60)) { + cleardevice(); + + auto newFrame = camera.grabFrame(3000); // 最多等待 3 秒 + if (!newFrame) { + fputs(TEXT_ERROR_GRAB_FAILED, stderr); + break; + } + + // 这里的 getImage 重复调用没有额外开销. + auto* img = newFrame->getImage(); + if (img) { + auto w = getwidth(img); + auto h = getheight(img); + putimage(0, 0, getwidth(), getheight(), img, 0, 0, w, h); + + // 更新当前分辨率索引 + currentResolutionIndex = findCurrentResolutionIndex(resolutions, w, h); + // 使用 shared_ptr 版本无需手动调用 release() + } + + // 处理键盘输入 + if (kbhit()) { + auto keyMsg = getkey(); + int key = keyMsg.key; + int newDeviceIndex = -1; + int newResolutionIndex = -1; + + if (key == ' ') { + // 空格键:切换到下一个相机 + newDeviceIndex = (currentDeviceIndex + 1) % deviceCount; + } else if (key >= '0' && key <= '9') { + // 数字键:切换到指定相机 + int requestedIndex = key - '0'; + if (requestedIndex < deviceCount) { + newDeviceIndex = requestedIndex; + } + } else if (key == '+' || key == '=') { + // '+' 键:暂未使用 + } else if (key == '-' || key == '_') { + // '-' 键:暂未使用 + } else if (key == key_up) { + // 上箭头键:切换到上一个分辨率 + if (!resolutions.empty()) { + newResolutionIndex = (currentResolutionIndex - 1 + static_cast(resolutions.size())) % static_cast(resolutions.size()); + } + } else if (key == key_down) { + // 下箭头键:切换到下一个分辨率 + if (!resolutions.empty()) { + newResolutionIndex = (currentResolutionIndex + 1) % static_cast(resolutions.size()); + } + } else if (key == key_esc) { + // ESC 键退出 + break; + } + + // 切换相机设备 + if (newDeviceIndex >= 0 && newDeviceIndex != currentDeviceIndex) { + printf(TEXT_SWITCHING_CAMERA, newDeviceIndex); + printf("\n"); + if (switchCamera(camera, newDeviceIndex, deviceCount)) { + currentDeviceIndex = newDeviceIndex; + // 获取新相机的分辨率列表 + resolutions = getResolutionList(camera); + currentResolutionIndex = 0; + } + } + + // 切换分辨率 + if (newResolutionIndex >= 0 && newResolutionIndex != currentResolutionIndex && !resolutions.empty()) { + int newWidth = resolutions[newResolutionIndex].width; + int newHeight = resolutions[newResolutionIndex].height; + printf(TEXT_SWITCHING_RESOLUTION, newWidth, newHeight); + printf("\n"); + if (switchCamera(camera, currentDeviceIndex, deviceCount, newWidth, newHeight)) { + currentResolutionIndex = newResolutionIndex; + // 调整窗口大小以匹配相机分辨率比例 + adjustWindowToCamera(newWidth, newHeight); + } + } + + flushkey(); + } + + // 显示相机设备列表(仅当有多个设备时显示切换提示) + int textY = 10; + setcolor(YELLOW); + + // 显示设备列表标题 + outtextxy(10, textY, TEXT_CAMERA_LIST_TITLE); + textY += 20; + + // 显示每个设备 + for (int i = 0; i < deviceCount; ++i) { + if (i == currentDeviceIndex) { + setcolor(LIGHTGREEN); + } else { + setcolor(WHITE); + } + sprintf(msgBuffer, TEXT_CAMERA_LIST_ITEM, i, deviceNames[i].c_str()); + outtextxy(10, textY, msgBuffer); + textY += 18; + } + + // 如果有多个相机,显示切换提示 + if (deviceCount > 1) { + textY += 5; + setcolor(CYAN); + sprintf(msgBuffer, TEXT_CAMERA_SWITCH, currentDeviceIndex, deviceNames[currentDeviceIndex].c_str()); + outtextxy(10, textY, msgBuffer); + textY += 20; + } + + // 显示分辨率列表 + if (!resolutions.empty()) { + textY += 10; + setcolor(YELLOW); + outtextxy(10, textY, TEXT_RESOLUTION_LIST_TITLE); + textY += 20; + + // 最多显示8个分辨率,避免占用太多屏幕空间 + int displayCount = std::min(static_cast(resolutions.size()), 8); + int startIndex = 0; + + // 如果分辨率列表很长,以当前选中的为中心显示 + if (resolutions.size() > 8) { + startIndex = std::max(0, currentResolutionIndex - 4); + startIndex = std::min(startIndex, static_cast(resolutions.size()) - 8); + } + + for (int i = startIndex; i < startIndex + displayCount && i < static_cast(resolutions.size()); ++i) { + if (i == currentResolutionIndex) { + setcolor(LIGHTGREEN); + sprintf(msgBuffer, TEXT_RESOLUTION_ITEM TEXT_RESOLUTION_CURRENT, resolutions[i].width, resolutions[i].height); + } else { + setcolor(WHITE); + sprintf(msgBuffer, TEXT_RESOLUTION_ITEM, resolutions[i].width, resolutions[i].height); + } + outtextxy(10, textY, msgBuffer); + textY += 16; + } + + // 显示分辨率切换提示 + textY += 5; + setcolor(CYAN); + outtextxy(10, textY, TEXT_RESOLUTION_SWITCH); + } + } + + fputs(TEXT_CAMERA_CLOSED, stderr); + camera.close(); + + return 0; +} + +#endif diff --git a/cmake_template/ege_demos/camera_wave.cpp b/cmake_template/ege_demos/camera_wave.cpp new file mode 100644 index 0000000..54c04d5 --- /dev/null +++ b/cmake_template/ege_demos/camera_wave.cpp @@ -0,0 +1,936 @@ +/** + * @file camera_wave.cpp + * @author wysaid (this@xege.org) + * @brief 一个使用 ege 内置的相机模块打开相机的例子 + * @date 2025-05-18 + * + */ + +#define SHOW_CONSOLE 1 + +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS 1 +#endif + +#ifndef NOMINMAX +#define NOMINMAX 1 +#endif + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +// 文本本地化宏定义 +#ifdef _MSC_VER +// MSVC编译器使用中文文案 +#define TEXT_WINDOW_TITLE "EGE 相机水波特效演示 - 作者: wysaid - 2025" +#define TEXT_ERROR_NO_CAMERA "此演示需要相机设备才能运行。\n请连接相机设备后重试。" +#define TEXT_ERROR_NO_CAMERA_FEATURE \ + "当前 ege 版本不支持相机功能,请使用支持 C++17 的编译器。 如果是 MSVC 编译器,请使用 MSVC 2022 或更高版本。" +#define TEXT_ERROR_EXIT_HINT "按任意键退出。" +#define TEXT_ERROR_NO_DEVICE "未找到相机设备!" +#define TEXT_ERROR_OPEN_FAILED "打开相机设备失败!" +#define TEXT_ERROR_GRAB_FAILED "获取帧数据失败!" +#define TEXT_CAMERA_CLOSED "相机设备已关闭!" +#define TEXT_CAMERA_DEVICE "相机设备: %s" +#define TEXT_CPP11_REQUIRED "需要 C++11 或更高版本。" +#define TEXT_INTENSITY_RULE "拖拽变形网格。弹性强度: %g" +#define TEXT_INFO_MSG "按 '+' 或 '-' 调整弹性, ↑/↓ 切换分辨率。作者: wysaid: http://xege.org" +#define TEXT_CAMERA_LIST_TITLE "可用相机设备:" +#define TEXT_CAMERA_LIST_ITEM " [%d] %s" +#define TEXT_CAMERA_SWITCH "按空格键切换相机,或按数字键选择 | 当前: [%d] %s" +#define TEXT_SWITCHING_CAMERA "正在切换到相机 %d..." +#define TEXT_RESOLUTION_LIST_TITLE "支持的分辨率:" +#define TEXT_RESOLUTION_ITEM " %dx%d" +#define TEXT_RESOLUTION_CURRENT " <-当前" +#define TEXT_SWITCHING_RESOLUTION "正在切换到分辨率 %dx%d..." +#define TEXT_WINDOW_RESIZED "窗口大小已调整为 %dx%d" +#else +// 非MSVC编译器使用英文文案 +#define TEXT_WINDOW_TITLE "EGE camera wave By wysaid - 2025" +#define TEXT_ERROR_NO_CAMERA "This demo requires a camera device to run.\nPlease connect a camera and try again." +#define TEXT_ERROR_NO_CAMERA_FEATURE \ + "The current version of EGE does not support camera features. Please use a compiler that supports C++17. If using MSVC, please use MSVC 2022 or later." +#define TEXT_ERROR_EXIT_HINT "Press any key to exit." +#define TEXT_ERROR_NO_DEVICE "No camera device found!!" +#define TEXT_ERROR_OPEN_FAILED "Failed to open camera device!!" +#define TEXT_ERROR_GRAB_FAILED "Failed to grab frame!!" +#define TEXT_CAMERA_CLOSED "Camera device closed!!" +#define TEXT_CAMERA_DEVICE "Camera device: %s" +#define TEXT_CPP11_REQUIRED "C++11 or higher is required." +#define TEXT_INTENSITY_RULE "Drag to deform mesh. Intensity: %g" +#define TEXT_INFO_MSG "Press '+'/'-' for elasticity, UP/DOWN for resolution. By wysaid: http://xege.org" +#define TEXT_CAMERA_LIST_TITLE "Available cameras:" +#define TEXT_CAMERA_LIST_ITEM " [%d] %s" +#define TEXT_CAMERA_SWITCH "Press SPACE to switch camera, or press number key | Current: [%d] %s" +#define TEXT_SWITCHING_CAMERA "Switching to camera %d..." +#define TEXT_RESOLUTION_LIST_TITLE "Supported resolutions:" +#define TEXT_RESOLUTION_ITEM " %dx%d" +#define TEXT_RESOLUTION_CURRENT " <-Current" +#define TEXT_SWITCHING_RESOLUTION "Switching to resolution %dx%d..." +#define TEXT_WINDOW_RESIZED "Window resized to %dx%d" +#endif + +// 判断一下 C++ 版本, 低于 C++11 的编译器不支持 +#if __cplusplus < 201103L +#pragma message("C++11 or higher is required.") + +int main() +{ + return 0; +} +#else + +#define WINDOW_WIDTH 1280 +#define WINDOW_HEIGHT 720 + +// 窗口尺寸限制 +#define MIN_LONG_EDGE 640 // 窗口长边最小值 +#define MAX_LONG_EDGE 1920 // 窗口长边最大值 + +/// 结合水波荡漾的 Demo, 给一个相机的版本 + +struct Point +{ + Point() : x(0), y(0), dx(0), dy(0) {} + + Point(float _x, float _y, float _u, float _v) : x(_x), y(_y), dx(0), dy(0), u(_u), v(_v) {} + + float x, y; + float dx, dy; + float u, v; +}; + +void my_line(int* data, int width, int height, int pnt1x, int pnt1y, int pnt2x, int pnt2y, int color) +{ + int dx = pnt2x - pnt1x; + int dy = pnt2y - pnt1y; + float x = pnt1x; + float y = pnt1y; + int step = 0; + if (abs(dx) > abs(dy)) { + step = abs(dx); + } else { + step = abs(dy); + } + + // 修复:添加除零检查 + if (step == 0) { + // 起点和终点相同,直接绘制单点 + if (pnt1x >= 0 && pnt1y >= 0 && pnt1x < width && pnt1y < height) { + data[pnt1x + pnt1y * width] = color; + } + return; + } + + float xStep = dx / (float)step; + float yStep = dy / (float)step; + for (int i = 0; i <= step; i++) { // 修复:应该包含终点,使用 <= + if (x >= 0 && y >= 0 && x < width && y < height) { // 修复:边界检查 + data[(int)x + (int)y * width] = color; + } + x += xStep; + y += yStep; + } +} + +class Net +{ +public: + Net() : m_index(0), m_intensity(0.2f), m_lastIndex(-1) {} + + ~Net() {} + + void setTextureImage(PIMAGE texture) + { + m_texture = texture; + m_texWidth = getwidth(texture); + m_texHeight = getheight(texture); + m_textureData = (color_t*)getbuffer(m_texture); + } + + void setOutputTarget(PIMAGE target) + { + m_outputTarget = target; + m_outputWidth = getwidth(target); + m_outputHeight = getheight(target); + } + + // 网格的分辨率 w x h + bool initNet(int w, int h, PIMAGE inputTexture, PIMAGE outputTarget) + { + if (w < 2 || h < 2) { + return false; + } + + if (inputTexture) { + setTextureImage(inputTexture); + } + + if (outputTarget) { + setOutputTarget(outputTarget); + } + + m_width = w; + m_height = h; + + m_vec[0].resize(w * h); + m_vec[1].resize(w * h); + float widthStep = 1.0f / (w - 1); + float heightStep = 1.0f / (h - 1); + + for (int i = 0; i != h; ++i) { + const float heightI = i * heightStep; + int index = w * i; + for (int j = 0; j != w; ++j) { + const float widthJ = j * widthStep; + m_vec[0][index] = Point(widthJ, heightI, widthJ, heightI); + m_vec[1][index] = Point(widthJ, heightI, widthJ, heightI); + + ++index; + } + } + return true; + } + + void update() + { + const float widthStep = 1.0f / (m_width - 1.0f); + const float heightStep = 1.0f / (m_height - 1.0f); + int index = (m_index + 1) % 2; + + for (int i = 1; i < m_height - 1; ++i) { + const int k = m_width * i; + for (int j = 1; j < m_width - 1; ++j) { + const int h = k + j; + float dx, dy; + dx = (m_vec[m_index][h - 1].x + m_vec[m_index][h + 1].x - m_vec[m_index][h].x * 2.0f); + dy = (m_vec[m_index][h - 1].y + m_vec[m_index][h + 1].y - m_vec[m_index][h].y * 2.0f); + + dx += (m_vec[m_index][h - m_width].x + m_vec[m_index][h + m_width].x - m_vec[m_index][h].x * 2.0f); + dy += (m_vec[m_index][h - m_width].y + m_vec[m_index][h + m_width].y - m_vec[m_index][h].y * 2.0f); + + // 模拟能量损失, 当加速度方向与速度方向相反时,加快减速 + if (std::signbit(dx) != std::signbit(m_vec[m_index][h].dx)) { + dx *= 1.0f + m_intensity; + } + + if (std::signbit(dy) != std::signbit(m_vec[m_index][h].dy)) { + dy *= 1.0f + m_intensity; + } + + m_vec[m_index][h].dx += dx * m_intensity; + m_vec[m_index][h].dy += dy * m_intensity; + m_vec[index][h].dx = m_vec[m_index][h].dx; + m_vec[index][h].dy = m_vec[m_index][h].dy; + + m_vec[index][h].x = m_vec[m_index][h].x + m_vec[index][h].dx; + m_vec[index][h].y = m_vec[m_index][h].y + m_vec[index][h].dy; + } + } + m_index = index; + } + + void catchPoint(float x, float y) + { + int index; + + if (m_lastIndex < 0) { + float mdis = 1e9f; + for (int i = 1; i < m_height - 1; ++i) { + const int k = m_width * i; + for (int j = 1; j < m_width - 1; ++j) { + const int h = k + j; + const float dis = fabsf(x - m_vec[m_index][h].x) + fabsf(y - m_vec[m_index][h].y); + if (dis < mdis) { + index = h; + mdis = dis; + } + } + } + m_lastIndex = index; + } else { + index = m_lastIndex; + } + + m_vec[0][index].x = x; + m_vec[0][index].y = y; + m_vec[1][index].x = x; + m_vec[1][index].y = y; + m_vec[0][index].dx = 0.0f; + m_vec[0][index].dy = 0.0f; + m_vec[1][index].dx = 0.0f; + m_vec[1][index].dy = 0.0f; + } + + void releasePoint() { m_lastIndex = -1; } + + template inline void fillTriangle(const Type& v0, const Type& v1, const Type& v2) + { + if (v0.y == v2.y) { + _fillSimpleTriangle(v0, v1, v2); + } else if (v1.y == v2.y) { + _fillSimpleTriangle(v1, v0, v2); + } else if (v0.y == v1.y) { + _fillSimpleTriangle(v0, v2, v1); + } else { + _fillNormalTriangle(v0, v1, v2); + } + } + + template inline void _fillSimpleTriangle(const Type& vv0, const Type& v1, const Type& vv2) + { + assert(vv0.y == vv2.y); + bool isOK = (vv0.x < vv2.x); + const Type& v0 = isOK ? vv0 : vv2; + const Type& v2 = isOK ? vv2 : vv0; + + float h = v1.y - v0.y; + + // 修复:添加除零检查 + if (fabs(h) < 1e-6f) { + return; // 三角形退化为线段,跳过 + } + + float dL = (v1.x - v0.x) / h; + float dR = (v1.x - v2.x) / h; + + float dUL = (v1.u - v0.u) / h; + float dUR = (v1.u - v2.u) / h; + + float dVL = (v1.v - v0.v) / h; + float dVR = (v1.v - v2.v) / h; + + float xL = v0.x, xR = v2.x; + float uL = v0.u, uR = v2.u; + float vL = v0.v, vR = v2.v; + + const color_t* data = m_textureData; + color_t* outputBuffer = (color_t*)getbuffer(m_outputTarget); + + if (v0.y < v1.y) { + for (int i = v0.y; i < v1.y; ++i) { + float len = xR - xL; + float uLen = uR - uL; + float vLen = vR - vL; + + // 修复:添加除零检查 + if (fabs(len) > 1e-6f) { + for (int j = xL; j < xR; ++j) { + float percent = (j - xL) / len; + float u = uL + uLen * percent; + float v = vL + vLen * percent; + if (u < 0 || v < 0 || u > 1 || v > 1 || i < 0 || j < 0 || i >= m_outputHeight || + j >= m_outputWidth) + { + continue; + } + + // 修复:添加纹理边界检查 + int ww = u * (m_texWidth - 1); + int hh = v * (m_texHeight - 1); + + int index = ww + hh * m_texWidth; + int outputIndex = j + i * m_outputWidth; + outputBuffer[outputIndex] = data[index]; + } + } + xL += dL; + xR += dR; + uL += dUL; + uR += dUR; + vL += dVL; + vR += dVR; + } + } else { + for (int i = v0.y; i > v1.y; --i) { + float len = xR - xL; + float uLen = uR - uL; + float vLen = vR - vL; + + // 修复:添加除零检查 + if (fabs(len) > 1e-6f) { + for (int j = xL; j < xR; ++j) { + float percent = (j - xL) / len; + float u = uL + uLen * percent; + float v = vL + vLen * percent; + if (u < 0 || v < 0 || u > 1 || v > 1 || i < 0 || j < 0 || i >= m_outputHeight || + j >= m_outputWidth) + { + continue; + } + + int ww = u * (m_texWidth - 1); + int hh = v * (m_texHeight - 1); + + int index = ww + hh * m_texWidth; + outputBuffer[j + i * m_outputWidth] = data[index]; + } + } + + xL -= dL; + xR -= dR; + uL -= dUL; + uR -= dUR; + vL -= dVL; + vR -= dVR; + } + } + } + + template inline void _fillNormalTriangle(const Type& v0, const Type& v1, const Type& v2) + { + const Type* pnts[] = {&v0, &v1, &v2}; + + if ((*pnts[0]).y > (*pnts[1]).y) { + std::swap(pnts[0], pnts[1]); + } + + if ((*pnts[0]).y > (*pnts[2]).y) { + std::swap(pnts[0], pnts[2]); + } + + if ((*pnts[1]).y > (*pnts[2]).y) { + std::swap(pnts[1], pnts[2]); + } + + const Type &vv0 = *pnts[0], &vv1 = *pnts[1], &vv2 = *pnts[2]; + + // 修复:添加除零检查 + float heightDiff = vv2.y - vv0.y; + if (fabs(heightDiff) < 1e-6f) { + return; // 三角形退化,跳过 + } + + Type newPoint; + + float percent = (vv1.y - vv0.y) / heightDiff; + + newPoint.x = floorf(vv0.x + (vv2.x - vv0.x) * percent); + newPoint.y = vv1.y; + newPoint.u = vv0.u + (vv2.u - vv0.u) * percent; + newPoint.v = vv0.v + (vv2.v - vv0.v) * percent; + + _fillSimpleTriangle(newPoint, vv0, vv1); + _fillSimpleTriangle(newPoint, vv2, vv1); + } + + void drawNet() + { + std::vector& vec = m_vec[m_index]; + int sz = vec.size(); + int i; // i变量前置, 方便vc6.0 编译 + m_pointCache.resize(sz); +#if _MSC_VER < 1600 // 兼容vc6.0 + Point* v = &m_pointCache[0]; + memcpy(v, &vec[0], sz * sizeof(vec[0])); +#else + Point* v = m_pointCache.data(); + memcpy(v, vec.data(), sz * sizeof(vec[0])); +#endif + + for (i = 0; i != sz; ++i) { + v[i].x = floorf(v[i].x * m_outputWidth); + v[i].y = floorf(v[i].y * m_outputHeight); + } + + for (i = 1; i != m_height; ++i) { + const int k1 = (i - 1) * m_width; + const int k2 = i * m_width; + + for (int j = 1; j != m_width; ++j) { + const int p1 = k1 + j - 1; + const int p2 = k1 + j; + const int p3 = k2 + j - 1; + const int p4 = k2 + j; + + fillTriangle(v[p1], v[p2], v[p3]); + fillTriangle(v[p3], v[p2], v[p4]); + } + } + + color_t* outputBuffer = (color_t*)getbuffer(m_outputTarget); + + if (m_lastIndex > 0) { + for (i = 0; i != m_height; ++i) { + const int k = i * m_width; + for (int j = 1; j != m_width; ++j) { + const int h = k + j; + // line(v[h - 1].x, v[h - 1].y, v[h].x, v[h].y, m_outputTarget); + my_line((int*)outputBuffer, m_outputWidth, m_outputHeight, v[h - 1].x, v[h - 1].y, v[h].x, v[h].y, + 0x00ffff00); + } + } + + for (i = 0; i != m_width; ++i) { + for (int j = 1; j != m_height; ++j) { + const int h2 = j * m_width + i; + const int h1 = (j - 1) * m_width + i; + // line(v[h1].x, v[h1].y, v[h2].x, v[h2].y, m_outputTarget); + my_line((int*)outputBuffer, m_outputWidth, m_outputHeight, v[h1].x, v[h1].y, v[h2].x, v[h2].y, + 0x00ffff00); + } + } + } + } + + void intensityInc(float f) + { + m_intensity += f; + if (m_intensity > 0.3f) { + m_intensity = 0.3f; + } + } + + void intensityDec(float f) + { + m_intensity -= f; + if (m_intensity < 0.001f) { + m_intensity = 0.001f; + } + } + + float getIntensity() { return m_intensity; } + +private: + std::vector m_vec[2]; + std::vector m_pointCache; + int m_index; + int m_width, m_height; + float m_intensity; + int m_lastIndex; + + PIMAGE m_texture; + int m_texWidth, m_texHeight; + color_t* m_textureData; + + PIMAGE m_outputTarget; + int m_outputWidth, m_outputHeight; +}; + +// 根据相机分辨率调整窗口大小,保持比例一致 +// 返回 true 表示窗口大小发生了变化 +bool adjustWindowToCamera(int cameraWidth, int cameraHeight, PIMAGE& target, Net& net) +{ + int windowWidth = getwidth(); + int windowHeight = getheight(); + + // 计算相机的长边和短边 + int cameraLongEdge = (std::max)(cameraWidth, cameraHeight); + int cameraShortEdge = (std::min)(cameraWidth, cameraHeight); + float cameraRatio = (float)cameraWidth / cameraHeight; + + // 获取屏幕可用区域(考虑任务栏) + RECT workArea; + SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0); + int screenAvailWidth = workArea.right - workArea.left; + int screenAvailHeight = workArea.bottom - workArea.top; + // 留一点边距,避免窗口贴边 + screenAvailWidth -= 20; + screenAvailHeight -= 40; + + // 计算目标窗口长边 + int targetLongEdge = cameraLongEdge; + + // 限制1:长边不能小于 MIN_LONG_EDGE + if (targetLongEdge < MIN_LONG_EDGE) { + targetLongEdge = MIN_LONG_EDGE; + } + + // 限制2:长边不能大于 MAX_LONG_EDGE + if (targetLongEdge > MAX_LONG_EDGE) { + targetLongEdge = MAX_LONG_EDGE; + } + + // 根据长边和比例计算窗口尺寸 + int newWidth, newHeight; + if (cameraWidth >= cameraHeight) { + // 横向视频,宽度是长边 + newWidth = targetLongEdge; + newHeight = (int)(newWidth / cameraRatio); + } else { + // 纵向视频,高度是长边 + newHeight = targetLongEdge; + newWidth = (int)(newHeight * cameraRatio); + } + + // 限制3:不能超过屏幕可用区域 + if (newWidth > screenAvailWidth) { + float scale = (float)screenAvailWidth / newWidth; + newWidth = screenAvailWidth; + newHeight = (int)(newHeight * scale); + } + if (newHeight > screenAvailHeight) { + float scale = (float)screenAvailHeight / newHeight; + newHeight = screenAvailHeight; + newWidth = (int)(newWidth * scale); + } + + // 如果新尺寸与当前尺寸相同,无需调整 + if (newWidth == windowWidth && newHeight == windowHeight) { + return false; + } + + // 调用 initgraph 调整窗口大小 (无需 closegraph) + initgraph(newWidth, newHeight, INIT_RENDERMANUAL); + setcaption(TEXT_WINDOW_TITLE); + setbkmode(TRANSPARENT); + + // 重新创建目标图像并重新初始化网格 + delimage(target); + target = newimage(newWidth, newHeight); + net.setOutputTarget(target); + + printf(TEXT_WINDOW_RESIZED, newWidth, newHeight); + printf("\n"); + + return true; +} + +void showErrorWindow() +{ + settarget(nullptr); + setbkcolor(BLACK); + cleardevice(); + setcolor(RED); + if (hasCameraCaptureModule()) { + outtextrect(0, 0, getwidth(), getheight(), TEXT_ERROR_NO_CAMERA); + } else { + outtextrect(0, 0, getwidth(), getheight(), TEXT_ERROR_NO_CAMERA_FEATURE); + } + outtextxy(10, 30, TEXT_ERROR_EXIT_HINT); + getch(); + closegraph(); +} + +// 切换相机设备的辅助函数 +bool switchCamera(ege::CameraCapture& camera, int deviceIndex, int deviceCount, int resWidth = WINDOW_WIDTH, int resHeight = WINDOW_HEIGHT) +{ + if (deviceIndex < 0 || deviceIndex >= deviceCount) { + return false; + } + + // 先关闭当前相机 + if (camera.isStarted()) { + camera.close(); + } + + // 设置相机分辨率 + camera.setFrameSize(resWidth, resHeight); + camera.setFrameRate(30); + + // 打开新相机 + if (!camera.open(deviceIndex)) { + return false; + } + + camera.start(); + return true; +} + +// 分辨率信息结构体 +struct ResolutionItem +{ + int width; + int height; +}; + +// 获取并保存分辨率列表 +std::vector getResolutionList(ege::CameraCapture& camera) +{ + std::vector resolutions; + auto resList = camera.getDeviceSupportedResolutions(); + if (resList.count > 0) { + for (int i = 0; i < resList.count; ++i) { + resolutions.push_back({resList.info[i].width, resList.info[i].height}); + } + } + return resolutions; +} + +// 根据当前帧分辨率找到对应的分辨率索引 +int findCurrentResolutionIndex(const std::vector& resolutions, int width, int height) +{ + for (size_t i = 0; i < resolutions.size(); ++i) { + if (resolutions[i].width == width && resolutions[i].height == height) { + return static_cast(i); + } + } + return 0; // 如果找不到,返回第一个 +} + +int main() +{ + /// 在相机的高吞吐场景下, 不设置 RENDERMANUAL 会出现闪屏. + initgraph(WINDOW_WIDTH, WINDOW_HEIGHT, INIT_RENDERMANUAL); + setcaption(TEXT_WINDOW_TITLE); + + Net net; + ege::CameraCapture camera; + PIMAGE target = newimage(getwidth(), getheight()); + char msgBuffer[1024]; + char intensityBuffer[256]; + + setbkmode(TRANSPARENT); + setcolor(YELLOW, target); + sprintf(intensityBuffer, TEXT_INTENSITY_RULE, net.getIntensity()); + + net.initNet(80, 60, nullptr, target); + + // 0: 不输出日志, 1: 输出警告日志, 2: 输出常规信息, 3: 输出调试信息, 超过 3 等同于 3. + ege::enableCameraModuleLog(2); + + // 保存设备信息 + std::vector deviceNames; + int deviceCount = 0; + int currentDeviceIndex = 0; + + // 分辨率相关 + std::vector resolutions; + int currentResolutionIndex = 0; + + { /// 获取所有相机设备的名称 + auto devices = camera.findDeviceNames(); + + if (devices.count == 0) { + fputs(TEXT_ERROR_NO_DEVICE, stderr); + showErrorWindow(); + return -1; + } + + deviceCount = devices.count; + for (int i = 0; i < devices.count; ++i) { + deviceNames.push_back(devices.info[i].name); + printf(TEXT_CAMERA_DEVICE, devices.info[i].name); + printf("\n"); + } + } + + // 打开第一个相机设备 + if (!switchCamera(camera, 0, deviceCount)) { + fputs(TEXT_ERROR_OPEN_FAILED, stderr); + return -1; + } + + // 获取分辨率列表 + resolutions = getResolutionList(camera); + + std::shared_ptr frame; + + { // 尝试获取一帧数据, 最多等五秒, 如果失败, 直接退出. + frame = camera.grabFrame(5000); + + if (!frame) { + fputs(TEXT_ERROR_GRAB_FAILED, stderr); + camera.close(); + return -1; + } + + net.setTextureImage(frame->getImage()); + } + + for (; camera.isStarted() && is_run(); delay_fps(60)) { + cleardevice(); + + // 0 表示不等待, 直接返回, 要处理一下返回值为空的情况. + auto newFrame = camera.grabFrame(0); + if (newFrame) { + // 使用 shared_ptr,无需手动释放旧帧 + frame = newFrame; + net.setTextureImage(frame->getImage()); + } + + if (!frame) { + fputs(TEXT_ERROR_GRAB_FAILED, stderr); + break; + } + + if (keystate(key_mouse_l)) { + int x, y; + mousepos(&x, &y); + net.catchPoint(x / (float)getwidth(), y / (float)getheight()); + } else { + net.releasePoint(); + } + + if (kbhit()) { + auto keyMsg = getkey(); + int key = keyMsg.key; + int newDeviceIndex = -1; + int newResolutionIndex = -1; + + switch (key) { + case '+': + case '=': + net.intensityInc(0.005f); + sprintf(intensityBuffer, TEXT_INTENSITY_RULE, net.getIntensity()); + break; + case '-': + case '_': + net.intensityDec(0.005f); + sprintf(intensityBuffer, TEXT_INTENSITY_RULE, net.getIntensity()); + break; + case key_up: + // 上箭头键:切换到上一个分辨率 + if (!resolutions.empty()) { + newResolutionIndex = (currentResolutionIndex - 1 + static_cast(resolutions.size())) % static_cast(resolutions.size()); + } + break; + case key_down: + // 下箭头键:切换到下一个分辨率 + if (!resolutions.empty()) { + newResolutionIndex = (currentResolutionIndex + 1) % static_cast(resolutions.size()); + } + break; + case ' ': + // 空格键:切换到下一个相机 + newDeviceIndex = (currentDeviceIndex + 1) % deviceCount; + break; + case key_esc: + exit(0); + default: + // 数字键:切换到指定相机 + if (key >= '0' && key <= '9') { + int requestedIndex = key - '0'; + if (requestedIndex < deviceCount) { + newDeviceIndex = requestedIndex; + } + } + break; + } + + // 切换相机设备 + if (newDeviceIndex >= 0 && newDeviceIndex != currentDeviceIndex) { + printf(TEXT_SWITCHING_CAMERA, newDeviceIndex); + printf("\n"); + + // 使用 shared_ptr,释放引用即可 + frame.reset(); + + if (switchCamera(camera, newDeviceIndex, deviceCount)) { + currentDeviceIndex = newDeviceIndex; + // 获取新相机的分辨率列表 + resolutions = getResolutionList(camera); + currentResolutionIndex = 0; + // 获取新相机的第一帧 + frame = camera.grabFrame(5000); + if (frame) { + net.setTextureImage(frame->getImage()); + } + } + } + + // 切换分辨率 + if (newResolutionIndex >= 0 && newResolutionIndex != currentResolutionIndex && !resolutions.empty()) { + int newWidth = resolutions[newResolutionIndex].width; + int newHeight = resolutions[newResolutionIndex].height; + printf(TEXT_SWITCHING_RESOLUTION, newWidth, newHeight); + printf("\n"); + + frame.reset(); + + if (switchCamera(camera, currentDeviceIndex, deviceCount, newWidth, newHeight)) { + currentResolutionIndex = newResolutionIndex; + // 调整窗口大小以匹配相机分辨率比例 + adjustWindowToCamera(newWidth, newHeight, target, net); + // 获取新分辨率的第一帧 + frame = camera.grabFrame(5000); + if (frame) { + net.setTextureImage(frame->getImage()); + } + } + } + + flushkey(); + } + + net.drawNet(); + net.update(); + putimage(0, 0, target); + + // 显示相机设备列表 + int textY = 10; + setcolor(0x00ff0000); // 红色 + outtextxy(10, textY, TEXT_INFO_MSG); + textY += 20; + outtextxy(10, textY, intensityBuffer); + textY += 25; + + // 显示设备列表标题 + setcolor(YELLOW); + outtextxy(10, textY, TEXT_CAMERA_LIST_TITLE); + textY += 18; + + // 显示每个设备 + for (int i = 0; i < deviceCount; ++i) { + if (i == currentDeviceIndex) { + setcolor(LIGHTGREEN); + } else { + setcolor(WHITE); + } + sprintf(msgBuffer, TEXT_CAMERA_LIST_ITEM, i, deviceNames[i].c_str()); + outtextxy(10, textY, msgBuffer); + textY += 16; + } + + // 如果有多个相机,显示切换提示 + if (deviceCount > 1) { + textY += 5; + setcolor(CYAN); + sprintf(msgBuffer, TEXT_CAMERA_SWITCH, currentDeviceIndex, deviceNames[currentDeviceIndex].c_str()); + outtextxy(10, textY, msgBuffer); + textY += 20; + } + + // 显示分辨率列表 + if (!resolutions.empty()) { + textY += 10; + setcolor(YELLOW); + outtextxy(10, textY, TEXT_RESOLUTION_LIST_TITLE); + textY += 18; + + // 获取当前帧的分辨率来更新索引 + if (frame && frame->getImage()) { + currentResolutionIndex = findCurrentResolutionIndex(resolutions, frame->getWidth(), frame->getHeight()); + } + + // 最多显示6个分辨率,避免占用太多屏幕空间 + int displayCount = std::min(static_cast(resolutions.size()), 6); + int startIndex = 0; + + // 如果分辨率列表很长,以当前选中的为中心显示 + if (resolutions.size() > 6) { + startIndex = std::max(0, currentResolutionIndex - 3); + startIndex = std::min(startIndex, static_cast(resolutions.size()) - 6); + } + + for (int i = startIndex; i < startIndex + displayCount && i < static_cast(resolutions.size()); ++i) { + if (i == currentResolutionIndex) { + setcolor(LIGHTGREEN); + sprintf(msgBuffer, TEXT_RESOLUTION_ITEM TEXT_RESOLUTION_CURRENT, resolutions[i].width, resolutions[i].height); + } else { + setcolor(WHITE); + sprintf(msgBuffer, TEXT_RESOLUTION_ITEM, resolutions[i].width, resolutions[i].height); + } + outtextxy(10, textY, msgBuffer); + textY += 14; + } + } + } + + fputs(TEXT_CAMERA_CLOSED, stderr); + camera.close(); + closegraph(); + + return 0; +} + +#endif diff --git a/cmake_template/ege_demos/game_gomoku.cpp b/cmake_template/ege_demos/game_gomoku.cpp new file mode 100644 index 0000000..2d34001 --- /dev/null +++ b/cmake_template/ege_demos/game_gomoku.cpp @@ -0,0 +1,737 @@ +/** + * @file game_gomoku.cpp + * @author wysaid (this@xege.org) + * @brief 传统规则下的五子棋游戏, 附带简单的AI对手 + * @version 0.1 + * @date 2025-06-18 + * + */ + +#include "graphics.h" +#include + +/// 是否禁用音效. 如果存在编译问题, 可以把下面这行的值改成 0 +#ifdef _MSC_VER +#define ENABLE_SOUNDS 1 +#else +// 如果使用非MSVC编译器, 需要手动链接 winmm.lib +// 这里默认禁用音效, 避免非MSVC编译器编译时出错 +#define ENABLE_SOUNDS 0 +#endif + +#if ENABLE_SOUNDS +#include +#include + +#pragma comment(lib, "winmm.lib") +#endif + +// 文本本地化宏定义 +#ifdef _MSC_VER +// MSVC编译器使用中文文案 +#define TEXT_WINDOW_TITLE "五子棋游戏 - EGE Demo" +#define TEXT_CURRENT_PLAYER "当前玩家: %s" +#define TEXT_GAME_MODE "游戏模式: %s" +#define TEXT_BLACK_PIECE "黑子" +#define TEXT_WHITE_PIECE "白子" +#define TEXT_BLACK_WIN "黑子获胜!" +#define TEXT_WHITE_WIN "白子获胜!" +#define TEXT_PLAYER_WIN "玩家获胜!" +#define TEXT_PLAYER_LOSE "玩家失败!" +#define TEXT_DRAW "平局!" +#define TEXT_EXIT_HINT "按 ESC 退出游戏, 按 R 切换先后手并重新开始" +#define TEXT_MODE_AI "人机对战 (按M切换)" +#define TEXT_MODE_HUMAN "双人对战 (按M切换)" +#define TEXT_FIRST_PLAYER "先手: %s" +#define TEXT_PLAYER_HUMAN "玩家" +#define TEXT_PLAYER_AI "AI" +#define TEXT_FONT_NAME "宋体" +#define TEXT_RESTART_MSG "游戏重新初始化..." +#else +// 非MSVC编译器使用英文文案 +#define TEXT_WINDOW_TITLE "Gomoku Game - EGE Demo" +#define TEXT_CURRENT_PLAYER "Current Player: %s" +#define TEXT_GAME_MODE "Game Mode: %s" +#define TEXT_BLACK_PIECE "Black" +#define TEXT_WHITE_PIECE "White" +#define TEXT_BLACK_WIN "Black Wins!" +#define TEXT_WHITE_WIN "White Wins!" +#define TEXT_PLAYER_WIN "Player Wins!" +#define TEXT_PLAYER_LOSE "Player Loses!" +#define TEXT_DRAW "Draw!" +#define TEXT_EXIT_HINT "Press ESC to Exit, Press R to Switch First Player and Restart" +#define TEXT_MODE_AI "VS AI (Press M to Switch)" +#define TEXT_MODE_HUMAN "VS Human (Press M to Switch)" +#define TEXT_FIRST_PLAYER "First: %s" +#define TEXT_PLAYER_HUMAN "Human" +#define TEXT_PLAYER_AI "AI" +#define TEXT_FONT_NAME "Arial" +#define TEXT_RESTART_MSG "Game Restarting..." +#endif + +// 游戏常量 +const int BOARD_SIZE = 15; // 棋盘大小 +const int CELL_SIZE = 32; // 格子大小 +const int BOARD_OFFSET_X = 50; // 棋盘X偏移 +const int BOARD_OFFSET_Y = 50; // 棋盘Y偏移 +const int WINDOW_WIDTH = BOARD_OFFSET_X * 2 + BOARD_SIZE * CELL_SIZE; +const int WINDOW_HEIGHT = BOARD_OFFSET_Y * 2 + BOARD_SIZE * CELL_SIZE + 100; // 额外空间显示信息 + +// 棋子类型 +enum PieceType +{ + EMPTY = 0, + BLACK_PIECE = 1, + WHITE_PIECE = 2 +}; + +// 游戏状态 +enum GameState +{ + PLAYING, + BLACK_WIN, + WHITE_WIN, + DRAW +}; + +// 游戏类 +class Gomoku +{ +public: + Gomoku() + { + initGame(); +#if ENABLE_SOUNDS + // 打开音频设备 + midiOutOpen(&m_device, m_deviceID, 0, 0, CALLBACK_NULL); +#endif + } + + // 初始化游戏 + void initGame(bool toggleFirst = false) + { + // 清空棋盘 + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + m_board[i][j] = EMPTY; + } + } + + // 重置最后落子位置 + m_lastRow = -1; + m_lastCol = -1; + + // 在AI模式下,如果需要轮换先手 + if (m_vsAI && toggleFirst) { + m_humanFirst = !m_humanFirst; + + if (!m_humanFirst) { + // AI先手,直接下在中心位置 + placePiece(BOARD_SIZE / 2, BOARD_SIZE / 2, BLACK_PIECE); + } + } + + // 根据先手设置确定当前玩家 + if (m_vsAI) { + m_currentPlayer = m_humanFirst ? BLACK_PIECE : WHITE_PIECE; + } else { + m_currentPlayer = BLACK_PIECE; // 双人模式总是黑子先行 + } + + m_gameState = PLAYING; + } + + // 绘制棋盘 + void drawBoard() + { + // 设置背景色为木色 + setbkcolor(EGERGB(222, 184, 135)); + cleardevice(); + + // 绘制棋盘线条 + setcolor(ege::BLACK); + setlinestyle(PS_SOLID, 2); + + // 绘制横线 + for (int i = 0; i < BOARD_SIZE; i++) { + int y = BOARD_OFFSET_Y + i * CELL_SIZE; + line(BOARD_OFFSET_X, y, BOARD_OFFSET_X + (BOARD_SIZE - 1) * CELL_SIZE, y); + } + + // 绘制竖线 + for (int j = 0; j < BOARD_SIZE; j++) { + int x = BOARD_OFFSET_X + j * CELL_SIZE; + line(x, BOARD_OFFSET_Y, x, BOARD_OFFSET_Y + (BOARD_SIZE - 1) * CELL_SIZE); + } + + // 绘制天元和星位 + setfillcolor(ege::BLACK); + int center = BOARD_SIZE / 2; + // 天元 + fillcircle(BOARD_OFFSET_X + center * CELL_SIZE, BOARD_OFFSET_Y + center * CELL_SIZE, 3); // 四个星位 + int starPos = 3; + fillcircle(BOARD_OFFSET_X + starPos * CELL_SIZE, BOARD_OFFSET_Y + starPos * CELL_SIZE, 2); + fillcircle(BOARD_OFFSET_X + (BOARD_SIZE - 1 - starPos) * CELL_SIZE, BOARD_OFFSET_Y + starPos * CELL_SIZE, 2); + fillcircle(BOARD_OFFSET_X + starPos * CELL_SIZE, BOARD_OFFSET_Y + (BOARD_SIZE - 1 - starPos) * CELL_SIZE, 2); + fillcircle(BOARD_OFFSET_X + (BOARD_SIZE - 1 - starPos) * CELL_SIZE, + BOARD_OFFSET_Y + (BOARD_SIZE - 1 - starPos) * CELL_SIZE, 2); + } + + // 绘制棋子 + void drawPieces() + { + ege_enable_aa(true); + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + if (m_board[i][j] != EMPTY) { + int x = BOARD_OFFSET_X + j * CELL_SIZE; + int y = BOARD_OFFSET_Y + i * CELL_SIZE; + + // 判断是否为最后一颗落子 + bool isLastPiece = (i == m_lastRow && j == m_lastCol); + + if (m_board[i][j] == BLACK_PIECE) { + setfillcolor(ege::BLACK); + setcolor(EGERGB(64, 64, 64)); + } else { + setfillcolor(ege::WHITE); + setcolor(EGERGB(192, 192, 192)); + } + + // 使用抗锯齿的EGE绘制函数,提供更平滑的圆形效果 + ege_fillcircle(x, y, CELL_SIZE / 2 - 2); + ege_circle(x, y, CELL_SIZE / 2 - 2); + + // 如果是最后一颗落子,绘制高亮标记 + if (isLastPiece) { + // 绘制红色小十字 + setcolor(EGERGB(255, 0, 0)); + setlinewidth(2); + line(x - CELL_SIZE / 6, y, x + CELL_SIZE / 6, y); + line(x, y - CELL_SIZE / 6, x, y + CELL_SIZE / 6); + } + } + } + } + ege_enable_aa(false); + } + + // 绘制游戏信息 + void drawInfo() + { + setcolor(ege::BLACK); + setfont(20, 0, TEXT_FONT_NAME); + + int infoY = BOARD_OFFSET_Y + BOARD_SIZE * CELL_SIZE + 20; + + if (m_gameState == PLAYING) { + xyprintf(BOARD_OFFSET_X, infoY, TEXT_CURRENT_PLAYER, + m_currentPlayer == BLACK_PIECE ? TEXT_BLACK_PIECE : TEXT_WHITE_PIECE); + xyprintf(BOARD_OFFSET_X, infoY + 25, TEXT_GAME_MODE, m_vsAI ? TEXT_MODE_AI : TEXT_MODE_HUMAN); + + // 在AI模式下显示先手信息 + if (m_vsAI) { + xyprintf( + BOARD_OFFSET_X, infoY + 50, TEXT_FIRST_PLAYER, m_humanFirst ? TEXT_PLAYER_HUMAN : TEXT_PLAYER_AI); + } + } else { + const char* winner = getWinnerText(); + xyprintf(BOARD_OFFSET_X, infoY, winner); + } + + xyprintf(BOARD_OFFSET_X, infoY + (m_vsAI && m_gameState == PLAYING ? 75 : 50), TEXT_EXIT_HINT); + } + + void drawGameOver() + { + if (m_gameState == PLAYING) { + if (m_gameEndImage != nullptr) { + // 清理缓存, 降低内存占用. + delimage(m_gameEndImage); + m_gameEndImage = nullptr; + } + return; // 游戏未结束,不绘制 + } + + if (m_gameEndImage == nullptr) { + int bgWidth = 300; + int bgHeight = 100; + + m_gameEndImage = newimage(bgWidth, bgHeight); + + settarget(m_gameEndImage); + + // 绘制半透明背景 + setfillcolor(0xffffffff); + setfillstyle(SOLID_FILL, 0xffffffff); + // 绘制背景矩形 + setlinewidth(3); + setbkmode(TRANSPARENT); + + // 设置大号字体 + setcolor(EGERGBA(255, 0, 0, 255)); // 红色文字 + setfont(48, 0, TEXT_FONT_NAME); + + // 根据游戏状态确定显示文本 + const char* gameOverText = getWinnerText(); + + fillrect(0, 0, bgWidth, bgHeight); + rectangle(0, 0, bgWidth, bgHeight); // 计算文本居中位置 + int textWidth = textwidth(gameOverText); + int textHeight = textheight(gameOverText); + int textX = (bgWidth - textWidth) / 2; + int textY = (bgHeight - textHeight) / 2; + + // 绘制游戏结束文本 + outtextxy(textX, textY, gameOverText); + m_imgX = (WINDOW_WIDTH - bgWidth) / 2; + m_imgY = (WINDOW_HEIGHT - bgHeight) / 2; + + ege_setalpha(0xa0, m_gameEndImage); // 设置半透明效果 + settarget(nullptr); // 结束绘制到临时图像 + } + + // putimage(m_imgX, m_imgY, m_gameEndImage); + putimage_withalpha(nullptr, m_gameEndImage, m_imgX, m_imgY); + } + + // 获取获胜者文案 + const char* getWinnerText() + { + if (m_gameState == DRAW) { + return TEXT_DRAW; + } + + if (m_vsAI) { + // AI模式下判断玩家是否获胜 + bool playerWins = false; + if (m_humanFirst && m_gameState == BLACK_WIN) { + playerWins = true; // 玩家是黑子且黑子获胜 + } else if (!m_humanFirst && m_gameState == WHITE_WIN) { + playerWins = true; // 玩家是白子且白子获胜 + } + return playerWins ? TEXT_PLAYER_WIN : TEXT_PLAYER_LOSE; + } else { + // 双人模式下使用传统文案 + if (m_gameState == BLACK_WIN) { + return TEXT_BLACK_WIN; + } else if (m_gameState == WHITE_WIN) { + return TEXT_WHITE_WIN; + } + } + + return TEXT_DRAW; + } + + // 鼠标点击转换为棋盘坐标 + bool mouseToBoard(int mouseX, int mouseY, int& row, int& col) + { + int x = mouseX - BOARD_OFFSET_X; + int y = mouseY - BOARD_OFFSET_Y; + + // 计算最近的交叉点 + col = (x + CELL_SIZE / 2) / CELL_SIZE; + row = (y + CELL_SIZE / 2) / CELL_SIZE; + + // 检查是否在棋盘范围内 + if (row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE) { + // 检查点击是否在交叉点附近 + int actualX = col * CELL_SIZE; + int actualY = row * CELL_SIZE; + int dx = x - actualX; + int dy = y - actualY; + + return (dx * dx + dy * dy <= (CELL_SIZE / 2) * (CELL_SIZE / 2)); + } + + return false; + } + + // 下棋 + bool placePiece(int row, int col, PieceType piece) + { + if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { + return false; + } + + if (m_board[row][col] != EMPTY) { + return false; + } + + m_board[row][col] = piece; + + // 更新最后落子位置 + m_lastRow = row; + m_lastCol = col; + + return true; + } + + // 检查是否五子连珠 + bool checkWin(int row, int col, PieceType piece) + { + // 四个方向:水平、垂直、主对角线、副对角线 + int directions[4][2] = {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; + + for (int dir = 0; dir < 4; dir++) { + int count = 1; // 包含当前棋子 + int dr = directions[dir][0]; + int dc = directions[dir][1]; + + // 正方向统计 + int r = row + dr, c = col + dc; + while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && m_board[r][c] == piece) { + count++; + r += dr; + c += dc; + } + + // 反方向统计 + r = row - dr, c = col - dc; + while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && m_board[r][c] == piece) { + count++; + r -= dr; + c -= dc; + } + + if (count >= 5) { + return true; + } + } + + return false; + } + + // 检查棋盘是否已满 + bool isBoardFull() + { + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + if (m_board[i][j] == EMPTY) { + return false; + } + } + } + return true; + } + + // 简单AI:寻找最佳落子位置 + void aiMove() + { + if (m_gameState != PLAYING) { + return; + } + + // 检查是否轮到AI + bool isAITurn = (m_humanFirst && m_currentPlayer == WHITE_PIECE) || + (!m_humanFirst && m_currentPlayer == BLACK_PIECE); + if (!isAITurn) { + return; + } + + int bestRow = -1, bestCol = -1; + int bestScore = -1000; + + // 遍历所有空位,评估每个位置的得分 + for (int i = 0; i < BOARD_SIZE; i++) { + for (int j = 0; j < BOARD_SIZE; j++) { + if (m_board[i][j] == EMPTY) { + int score = evaluatePosition(i, j); + if (score > bestScore) { + bestScore = score; + bestRow = i; + bestCol = j; + } + } + } + } + + // 如果找到了合适的位置,就下棋 + if (bestRow != -1 && bestCol != -1) { + makeMove(bestRow, bestCol); + } + } + + // 评估位置得分 + int evaluatePosition(int row, int col) + { + int score = 0; + + // 基础得分:靠近中心的位置更好 + int centerRow = BOARD_SIZE / 2; + int centerCol = BOARD_SIZE / 2; + int distanceFromCenter = abs(row - centerRow) + abs(col - centerCol); + score += (BOARD_SIZE - distanceFromCenter); + + // 获取AI的棋子类型 + PieceType aiPiece = m_humanFirst ? WHITE_PIECE : BLACK_PIECE; + PieceType humanPiece = m_humanFirst ? BLACK_PIECE : WHITE_PIECE; + + // 攻击得分:评估AI能形成的连珠 + score += evaluateDirection(row, col, aiPiece) * 10; + + // 防守得分:阻止玩家形成连珠 + score += evaluateDirection(row, col, humanPiece) * 8; + + return score; + } + + // 评估方向得分 + int evaluateDirection(int row, int col, PieceType piece) + { + int score = 0; + int directions[4][2] = {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; + + for (int dir = 0; dir < 4; dir++) { + int dr = directions[dir][0]; + int dc = directions[dir][1]; + + // 统计连续的同色棋子 + int count = 0; + bool blocked = false; + + // 正方向 + int r = row + dr, c = col + dc; + while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) { + if (m_board[r][c] == piece) { + count++; + } else if (m_board[r][c] != EMPTY) { + blocked = true; + break; + } else { + break; + } + r += dr; + c += dc; + } + + // 反方向 + r = row - dr, c = col - dc; + while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) { + if (m_board[r][c] == piece) { + count++; + } else if (m_board[r][c] != EMPTY) { + if (!blocked) { + blocked = true; + } + break; + } else { + break; + } + r -= dr; + c -= dc; + } + + // 根据连子数量和是否被阻挡给分 + if (count >= 4) { + score += 1000; // 四连,非常重要 + } else if (count >= 3) { + score += blocked ? 5 : 50; + } else if (count >= 2) { + score += blocked ? 2 : 10; + } else if (count >= 1) { + score += blocked ? 1 : 3; + } + } + + return score; + } + + // 执行移动 + void makeMove(int row, int col) + { + if (placePiece(row, col, m_currentPlayer)) { + if (checkWin(row, col, m_currentPlayer)) { + m_gameState = (m_currentPlayer == BLACK_PIECE) ? BLACK_WIN : WHITE_WIN; + } else if (isBoardFull()) { + m_gameState = DRAW; + } else { + // 切换玩家 + m_currentPlayer = (m_currentPlayer == BLACK_PIECE) ? WHITE_PIECE : BLACK_PIECE; + } + } + + playPieceSound(m_currentPlayer); // 播放下棋音效 + } + + // 处理鼠标点击 + void handleMouseClick(int mouseX, int mouseY) + { + if (m_gameState != PLAYING) { + return; + } + + // 如果是AI回合,忽略鼠标点击 + if (m_vsAI && + ((m_humanFirst && m_currentPlayer == WHITE_PIECE) || (!m_humanFirst && m_currentPlayer == BLACK_PIECE))) + { + return; + } + + int row, col; + if (mouseToBoard(mouseX, mouseY, row, col)) { + makeMove(row, col); + } + } + + // 获取当前游戏状态 + GameState getGameState() const { return m_gameState; } + + // 获取当前玩家 + PieceType getCurrentPlayer() const { return m_currentPlayer; } + + // 是否为AI模式 + bool isVsAI() const { return m_vsAI; } + + // 切换游戏模式 + void toggleMode() + { + m_vsAI = !m_vsAI; + if (m_vsAI) { + m_humanFirst = true; + } + initGame(); + } + + // 重新开始游戏(在AI模式下轮换先手) + void restartGame() + { + if (m_vsAI) { + initGame(true); // 轮换先手 + } else { + initGame(); // 不轮换先手 + } + } + + enum // MIDI音符编号定义 + { + MIDI_BLACK = 45, + MIDI_WHITE = 57, + }; + + // 播放声音 + void playPieceSound(PieceType piece) + { +#if ENABLE_SOUNDS + if (m_device == nullptr) { + return; + } + // 设置音色为木琴(Xylophone),音色编号13,更符合棋子落盘的感觉 + DWORD msg = 0xC000 | 13; // 0xC0 是更改乐器的控制命令,13 是木琴的乐器号 + midiOutShortMsg(m_device, msg); + + if (m_lastSound != 0) { + // 如果上一个音符还在播放,先停止它 + midiOutShortMsg(m_device, 0x80 | (m_lastSound << 8)); // 0x80是音符关闭命令 + } + + // 播放黑子下棋音效 - 使用较低沉的G4音符 + if (piece == BLACK_PIECE) { + // 0x90是音符开启命令,80是适中的音量(比127更柔和) + midiOutShortMsg(m_device, 0x90 | (MIDI_BLACK << 8) | (80 << 16)); + m_lastSound = MIDI_BLACK; // 记录上一个音符 + } else if (piece == WHITE_PIECE) { + // 播放白子下棋音效 - 使用较清脆的C5音符,与G4形成完美四度音程 + midiOutShortMsg(m_device, 0x90 | (MIDI_WHITE << 8) | (80 << 16)); + m_lastSound = MIDI_WHITE; // 记录上一个音符 + } + m_soundTimer = 20; // 音效持续20帧 +#endif + } + + void updatePieceSound() + { +#if ENABLE_SOUNDS + if (m_soundTimer <= 0 || m_device == nullptr) { + return; + } + --m_soundTimer; + if (m_soundTimer == 0 && m_lastSound != 0) { + // 停止上一个音符 + midiOutShortMsg(m_device, 0x80 | (m_lastSound << 8)); // 0x80是音符关闭命令 + m_lastSound = 0; // 重置上一个音符 + } +#endif + } + +private: + int m_board[BOARD_SIZE][BOARD_SIZE]; // 棋盘数组 + PIMAGE m_gameEndImage{}; // 用于临时缓存绘制 + int m_imgX{}, m_imgY{}; // 用于结束动画 + PieceType m_currentPlayer{}; // 当前玩家 + GameState m_gameState{}; // 游戏状态 + bool m_vsAI = true; // 是否对战AI + bool m_humanFirst = true; // AI模式下是否玩家先手 + + // 最后落子位置记录 + int m_lastRow = -1; // 最后落子的行位置 + int m_lastCol = -1; // 最后落子的列位置 + +#if ENABLE_SOUNDS + /// 音频相关 + HMIDIOUT m_device{}; // MIDI输出设备句柄 + UINT m_deviceID = 0; // 使用默认设备 + int m_soundTimer = 0; // 音效倒计时, 到0时停止 + DWORD m_lastSound{}; // 上一个音符 +#endif +}; + +int main() +{ + // 初始化图形窗口 + initgraph(WINDOW_WIDTH, WINDOW_HEIGHT, INIT_RENDERMANUAL); + setrendermode(RENDER_MANUAL); + setcaption(TEXT_WINDOW_TITLE); + + Gomoku game; + + // 游戏主循环 + while (is_run()) { + // 绘制游戏画面 + game.drawBoard(); + game.drawPieces(); + game.drawInfo(); + game.updatePieceSound(); // 更新音效状态 + + GameState state = game.getGameState(); + if (state == PLAYING) { + // AI移动(如果是AI回合) + if (game.isVsAI()) { + game.aiMove(); + } + } else { + game.drawGameOver(); + } + + // 处理鼠标消息 + while (mousemsg()) { + mouse_msg msg = getmouse(); + if (msg.is_down() && msg.is_left()) { + game.handleMouseClick(msg.x, msg.y); + } + } + + // 处理键盘消息 + if (kbhit()) { + int key = getch(); + if (key == 27) { // ESC键退出 + break; + } else if (key == 'r' || key == 'R') { // R键重新开始 + cleardevice(0); + /// 打印重新初始化消息,提醒玩家游戏重开了 + setcolor(EGERGB(255, 0, 0)); + setfont(50, 0, TEXT_FONT_NAME); + xyprintf(50, WINDOW_HEIGHT / 2, TEXT_RESTART_MSG); + Sleep(500); // 稍等一会, 让玩家感知到游戏重开了. + game.restartGame(); + } else if (key == 'm' || key == 'M') { // M键切换模式 + game.toggleMode(); + } + } + + delay_fps(60); + } + + closegraph(); + return 0; +} \ No newline at end of file diff --git a/cmake_template/ege_demos/game_snake.cpp b/cmake_template/ege_demos/game_snake.cpp new file mode 100644 index 0000000..c81b3f3 --- /dev/null +++ b/cmake_template/ege_demos/game_snake.cpp @@ -0,0 +1,91 @@ +// 超简易90行贪吃蛇 +#include +#include + +#define MAP_W 40 +#define MAP_H 30 +const color_t GCOLOR[] = {DARKGRAY, GREEN, RED}; + +int gw, gh; + +struct SNAKE { + int dir, head, inc, tail; + int pool[MAP_W* MAP_H]; +} game; + +inline void drawAt( const int &i ) { + int x = ( i % MAP_W ) * gw, y = ( i / MAP_W ) * gh; + setfillcolor( GCOLOR[game.pool[i] >> 16] ); + bar( x, y, x + gw, y + gh ); +} + +void newFruit( void ) { + int nf; + while ( game.pool[nf = random( MAP_W * MAP_H )] >> 16 ); + game.pool[nf] = 0x20000, drawAt( nf ); +} + +int moveSnake( const int dx, const int dy, const bool u = false ) { + if ( u && dx + ( game.dir & 3 ) == 1 && dy + ( game.dir >> 2 ) == 1 ) return 1; + int nh; + if ( dx && !dy ) { + nh = game.head % MAP_W + dx; + if ( nh < 0 || nh >= MAP_W ) return 0; + nh = game.head + dx; + } else { + nh = game.head / MAP_W + dy; + if ( nh < 0 || nh >= MAP_H ) return 0; + nh = game.head + dy * MAP_W; + } + int s = game.pool[nh] >> 16; + if ( s == 1 ) return 0; + if ( s == 2 ) game.inc += 5, newFruit(); + if ( game.inc > 0 ) --game.inc; + else { + game.tail = game.pool[s = game.tail] & 0xffff; + game.pool[s] = 0, drawAt( s ); + } + game.pool[game.head] |= nh; + game.pool[game.head = nh] = 0x10000, drawAt( nh ); + game.dir = ( dx + 1 ) | ( ( dy + 1 ) << 2 ); + return 1; +} + +void gameInit( void ) { + int data[] = {6, 0, 2, 0, 0x10000}; + memset( game.pool, 0, sizeof( game.pool ) ); + memmove( &game, data, sizeof( data ) ); +} + +void gameScene( void ) { + setbkcolor( DARKGRAY ); + setfillcolor( GREEN ); + bar( 0, 0, gw, gh ); + newFruit(); + for ( int c = -1; is_run(); delay_fps( 60 ), --c ) { + while ( kbhit() ) { + int key = getch() | 0x20; + if ( key == ( 27 | 0x20 ) ) return; + if ( key == 'a' || key == 'd' ) { + if ( !moveSnake( ( ( key - 'a' ) >> 1 << 1 ) - 1, 0, true ) ) return; + } else if ( key == 's' || key == 'w' ) { + if ( !moveSnake( 0, 1 - ( ( key - 's' ) >> 2 << 1 ), true ) ) return; + } + } + if ( c < 0 ) { + if ( !moveSnake( ( game.dir & 3 ) - 1, ( game.dir >> 2 ) - 1 ) ) return; + c = 20; + } + } +} + +int main( void ) { + setinitmode( INIT_ANIMATION ); + initgraph( 640, 480 ); + gw = getwidth() / MAP_W, gh = getheight() / MAP_H; + randomize(); + gameInit(); + gameScene(); + return 0; +} + diff --git a/cmake_template/ege_demos/game_tetris.cpp b/cmake_template/ege_demos/game_tetris.cpp new file mode 100644 index 0000000..96a2df8 --- /dev/null +++ b/cmake_template/ege_demos/game_tetris.cpp @@ -0,0 +1,395 @@ +//ege俄罗斯方块游戏 +#include + +#include +#include +#include + +#include + +const int g_width = 400; +const int g_height = 520; + +/* 记录7种形状及其4种变化的表 */ +static int g_trs_map[8][4][4][4]; +/* 变化数目表 */ +static int g_map_mod[] = {1, 4, 4, 4, 2, 2, 2, 1, 0}; + +/* 初始化全局数据及图形显示 */ +void initgr() { + initgraph(g_width, g_height); + setfont(12, 6, "宋体"); + int Trs_map[8][4][4][4] = + { + {{{0}}},{{ + {0,0,0,0},{1,1,1,0},{0,1,0,0}, + },{ + {0,1,0,0},{1,1,0,0},{0,1,0,0}, + },{ + {0,1,0,0},{1,1,1,0}, + },{ + {0,1,0,0},{0,1,1,0},{0,1,0,0}, + },},{{ + {2,2,0,0},{0,2,0,0},{0,2,0,0}, + },{ + {0,0,2,0},{2,2,2,0}, + },{ + {0,2,0,0},{0,2,0,0},{0,2,2,0}, + },{ + {0,0,0,0},{2,2,2,0},{2,0,0,0}, + },},{{ + {0,3,3,0},{0,3,0,0},{0,3,0,0}, + },{ + {0,0,0,0},{3,3,3,0},{0,0,3,0}, + },{ + {0,3,0,0},{0,3,0,0},{3,3,0,0}, + },{ + {3,0,0,0},{3,3,3,0}, + },},{{ + {4,4,0,0},{0,4,4,0}, + },{ + {0,0,4,0},{0,4,4,0},{0,4,0,0}, + },},{{ + {0,5,5,0},{5,5,0,0}, + },{ + {0,5,0,0},{0,5,5,0},{0,0,5,0}, + },},{{ + {0,0,0,0},{6,6,6,6}, + },{ + {0,0,6,0},{0,0,6,0},{0,0,6,0},{0,0,6,0}, + },},{{ + {0,0,0,0},{0,7,7,0},{0,7,7,0}, + },}, + }; + memcpy(g_trs_map, Trs_map, sizeof(Trs_map)); +} + +class Game { +public: + /* 状态表 */ + enum { + ST_START, /* 游戏重新开始 */ + ST_NEXT, /* 准备下一个方块 */ + ST_NORMAL,/* 玩家控制阶段 */ + ST_OVER /* 游戏结束,F2重新开始 */ + }; + Game(int w, int h, int bw, int bh) { + int colormap[10] = {0, 0xA00000, 0xA05000, 0xA0A000, 0xC000, + 0x00A0A0, 0x4040C0, 0xA000A0, 0x808080, 0xFFFFFF}; + memcpy(m_colormap, colormap, sizeof(m_colormap)); + + int Keys[8] = {VK_F2,VK_LEFT,VK_RIGHT,VK_DOWN,VK_UP,VK_NUMPAD0,VK_SPACE}; + memcpy(m_Keys, Keys, sizeof(Keys)); + + memset(m_KeyState, 0, sizeof(m_KeyState)); + m_gamepool_w = w; + m_gamepool_h = h; + m_base_w = bw; + m_base_h = bh; + + randomize(); + m_ctl_t = -1; + m_pcb = newimage(); + for (int i=0; i<10; ++i) { + drawtile(bw * i, 0, bw, bh, 5, colormap[i]); + } + getimage(m_pcb, 0, 0, bw*10, bh); + m_state = ST_START; + } + /* 状态转换处理 */ + int deal () { + int nRet = 0; + if ( m_state == ST_START ) { //初始化 + m_next1_s = random(7) + 1; + m_next2_s = random(7) + 1; + m_pause = 0; + memset(m_gamepool, 255, sizeof(m_gamepool)); + for (int y = 1; y <= m_gamepool_h; ++y) { + for (int x = 1; x <= m_gamepool_w; ++x) + m_gamepool[y][x] = 0; + } + m_forbid_down = 0; + m_ctl_t = -1; + nRet = 1; + m_state = ST_NEXT; + } else if ( m_state == ST_NEXT ) { + m_ctl_x = (m_gamepool_w - 4) / 2 + 1; + m_ctl_y = 1; + m_ctl_t = 0; + m_ctl_s = m_next1_s; + m_ctl_dy = 0; + m_next1_s = m_next2_s; + m_next2_s = random(7) + 1; + m_curtime = m_droptime; + m_curxtime = 0; + nRet = 1; + if ( isCrash() ) { + m_gray_y = m_gamepool_h * 2; + m_over_st = 0; + m_state = ST_OVER; + } else { + m_state = ST_NORMAL; + } + } else if (m_state == ST_NORMAL) { + /* 处理自由下落 */ + int i, j; + if ( m_KeyState[3] == 0 || m_forbid_down) { + --m_curtime, m_cursubtime = 1; + } + if ( m_curxtime ) { + if (m_curxtime<0) + m_curxtime++; + else + m_curxtime--; + } + /* 按键处理 */ + for (i = 1, j = 1; i<=2; ++i, j-=2) { + for ( ; m_KeyFlag[i] > 0; --m_KeyFlag[i]) { + m_ctl_x -= j; + if ( isCrash() ) + m_ctl_x += j; + else + m_curxtime = m_movxtime * j; + } + } + m_ctl_dx = float(double(m_curxtime) / m_movxtime); //处理x方向平滑 + for (i = 4, j = 1; i<=5; ++i, j-=2) { + for (int t ; m_KeyFlag[i] > 0; --m_KeyFlag[i]) { + m_ctl_t=((t=m_ctl_t)+g_map_mod[m_ctl_s]+j)%g_map_mod[m_ctl_s]; + if ( isCrash() ) m_ctl_t = t; + } + } + if ( m_forbid_down == 0 && (m_KeyState[3] ) ) { + m_curtime -= m_cursubtime++; + } + if (m_curtime<0) { + ++m_ctl_y; + if ( isCrash() ) { + --m_ctl_y; + merge(); + m_ctl_dy = 0; m_ctl_dx = 0; m_ctl_t = -1; + if ( m_KeyState[3] ) + m_forbid_down = 1; + m_state = ST_NEXT; + } else { + m_curtime += m_droptime; + } + } + if (m_state == ST_NORMAL) { + m_ctl_dy = float(double(m_curtime) / m_droptime);//处理y方向平滑 + } + } else if (m_state == ST_OVER) { + if ( m_gray_y>0 && (m_gray_y % 2) == 0) + for (int x = 1; x <= m_gamepool_w; ++x) + if ( m_gamepool[m_gray_y>>1][x] ) + m_gamepool[m_gray_y>>1][x] = 8; + m_gray_y--; + ++m_over_st; + if ( m_KeyFlag[0] > 0 ) + m_state = ST_START; + } + memset(m_KeyFlag, 0, sizeof(m_KeyFlag)); + return nRet; + } + /* 碰撞检测 */ + bool isCrash() { + for (int y=0; y<4; ++y) { + for (int x=0; x<4; ++x) + if ( g_trs_map[m_ctl_s][m_ctl_t][y][x] ) { + if ( m_ctl_y + y < 0 || m_ctl_x + x < 0 + || m_gamepool[m_ctl_y + y][m_ctl_x + x]) + return true; + } + } + return false; + } + void merge() { + int y, x, cy = m_gamepool_h; + /* 合并处理 */ + for (y=0; y<4; ++y) { + for (x=0; x<4; ++x) + if ( g_trs_map[m_ctl_s][m_ctl_t][y][x] ) + m_gamepool[m_ctl_y + y][m_ctl_x + x] + = g_trs_map[m_ctl_s][m_ctl_t][y][x]; + } + /* 消行计算 */ + for (y = m_gamepool_h; y >= 1; --y) { + for (x = 1; x <= m_gamepool_w; ++x) { + if ( m_gamepool[y][x] == 0 ) + break; + } + if ( x <= m_gamepool_w ) { + if ( cy != y ) { + for (x = 1; x <= m_gamepool_w; ++x) + m_gamepool[cy][x] = m_gamepool[y][x]; + } + --cy; + } + } + for (y = cy; y >= 1; --y) { + for (x = 1; x <= m_gamepool_w; ++x) + m_gamepool[y][x] = 0; + } + } + /* 逻辑更新主函数 */ + void update() { + key_msg key; + int k = kbmsg(); + while ( k ) { + key = getkey(); + for (int i=0; i<8; ++i) { + if (key.key == m_Keys[i]) { + if (key.msg == key_msg_down) { + m_KeyFlag[i]++; + m_KeyState[i] = 1; + } else if (key.msg == key_msg_up) { + m_KeyFlag[i] = 0; + m_KeyState[i] = 0; + if ( i == 3 ) + m_forbid_down = 0; + } + } + } + k = kbmsg(); + } + while ( deal() ); + } + void drawedge(int x, int y, int w, int h, int color, int bdark = 1) { + setcolor(getchangcolor(color, 1.5f)); + moveto(x, y+h); + lineto(x, y); + lineto(x+w, y); + if ( bdark ) + setcolor(getchangcolor(color, 0.7f)); + lineto(x+w, y+h); + lineto(x, y+h); + } + void drawtile(int x, int y, int w, int h, int d, int color) { + w--, h--; + setfillcolor(color); + bar(x+1, y+1, x+w, y+h); + drawedge(x, y, w, h, color); + drawedge(x+1, y+1, w-2, h-2, color); + } + void drawframe(int x, int y, int w, int h, int d = 0) { + int coll[] = {0x400040, 0x600060, 0xA000A0, 0xFF00FF, + 0xA000A0, 0x600060, 0x400040}; + setfillcolor(0x010101); + bar(x, y, x + w--, y + h--); + for (int i=0; i<7; ++i) { + --x, --y, w += 2, h += 2; + drawedge(x, y, w, h, coll[i], 0); + } + } + void draw44(int bx, int by, int mat[4][4], + float dx=0, float dy=0, int nc=0, int deep=5) { + for (int y = 3; y >= 0; --y) { + for (int x = 0, c; x < 4; ++x) { + if ( c = mat[y][x] ) { + if ( nc ) c = nc; + drawtile(int(bx + (x + dx) * m_base_w + 1000.5) - 1000, + int(by + (y - dy) * m_base_h + 1000.5) - 1000, + m_base_w, m_base_h, deep, + m_colormap[c]); + } + } + } + } + /* 图形更新主函数 */ + void render() { + int x, y, c, bx, by; + /* 画背景框 */ + cleardevice(); + + drawframe(m_base_x + 5 * m_base_w, m_base_y, m_gamepool_w * m_base_w, m_gamepool_h * m_base_h); + drawframe(m_base_x, m_base_y, 4*m_base_w, 4*m_base_h); + drawframe(m_base_x, m_base_y + 5*m_base_h, 4*m_base_w, 4*m_base_h); + + /* 画主游戏池 */ + bx = m_base_x + 4 * m_base_w; + by = m_base_y - 1 * m_base_h; + for (y = m_gamepool_h; y >= 1; --y) { + for (x = 1; x <= m_gamepool_w; ++x) { + if ( c = m_gamepool[y][x] ) + putimage(bx + x * m_base_w, by + y * m_base_h, + m_base_w, m_base_h, m_pcb, + c * m_base_w, 0); + } + } + /* 画控制块 */ + if ( m_ctl_t >=0 ) { + bx = m_base_x + (m_ctl_x + 4) * m_base_w; + by = m_base_y + (m_ctl_y - 1) * m_base_h; + draw44(bx, by, g_trs_map[m_ctl_s][m_ctl_t], m_ctl_dx, m_ctl_dy); + } + /* 画下一块和下二块 */ + bx = m_base_x; + by = m_base_y; + draw44(bx, by, g_trs_map[m_next1_s][0]); + bx = m_base_x; + by = m_base_y + 5 * m_base_h; + draw44(bx, by, g_trs_map[m_next2_s][0], 0, 0, 8); + setcolor(0xFFFFFF); + if ( m_state == ST_OVER ) { // 结束提示文字显示 + outtextxy(m_base_x+5*m_base_w, m_base_y, "Press F2 to Restart game"); + } + } + static int dealbit(int a, float dt) { + a = int(a * dt); + if ( a>255 ) a = 255; + else if ( a<0 ) a = 0; + return a; + } + static int getchangcolor(int Color, float t) { + int r = EGEGET_R(Color), g = EGEGET_G(Color), b = EGEGET_B(Color); + r = dealbit(r, t); + g = dealbit(g, t); + b = dealbit(b, t); + return EGERGB(r, g, b); + } +public: + int m_base_x, m_base_y, m_base_w, m_base_h; + int m_droptime; + int m_curtime; + int m_cursubtime; + int m_movxtime; + int m_curxtime; +private: + int m_gamepool_w, m_gamepool_h; + int m_gamepool[30][30]; //从1为起始下标,0用于边界碰撞检测 + int m_ctl_x, m_ctl_y, m_ctl_t, m_ctl_s; //当前控制块属性 + float m_ctl_dx, m_ctl_dy; + int m_next1_s, m_next2_s; + int m_forbid_down; + int m_colormap[10]; +public: + int m_pause; + int m_state; //游戏主状态 + int m_gray_y; + int m_over_st; + int m_Keys[8]; + int m_KeyFlag[8]; + int m_KeyState[8]; +private: + PIMAGE m_pcb; +}; + +int main() { + int nfps = 120; + initgr(); + + fps ui_fps; + Game game(10, 20, 24, 24); + game.m_base_x = 20; + game.m_base_y = 20; + game.m_droptime = nfps / 2; + game.m_movxtime = 10; + + setrendermode(RENDER_MANUAL); + for ( ; is_run(); delay_fps(nfps)) { + game.update(); + game.render(); + } + return 0; +} + diff --git a/cmake_template/ege_demos/game_type.cpp b/cmake_template/ege_demos/game_type.cpp new file mode 100644 index 0000000..6ffde31 --- /dev/null +++ b/cmake_template/ege_demos/game_type.cpp @@ -0,0 +1,82 @@ +//打字小游戏 +#include +typedef struct chartarget +{ + float x, y; + float dy; + char c; + char vis; +}chartarget; +int movechar(chartarget* ct) +{ + ct->y += ct->dy; + if (ct->y > getheight() || ct->vis == 0) return 1; + return 0; +} +void drawchar(chartarget* ct) +{ + outtextxy((int)ct->x, (int)ct->y, ct->c); +} +void movechars(int* nobj, chartarget* ct, int bnew) +{ + int i; + if (bnew) + { + int n = (*nobj)++; + ct[n].x = (float)(random(getwidth() - 40) + 20 - 9); + ct[n].y = -50; + ct[n].dy = (float)(randomf() * 3 + 1); + ct[n].c = random(26) + 'a'; + ct[n].vis = 1; + } + for (i = 0; i < *nobj; ++i) + { + if (movechar(ct + i)) + { + ct[i] = ct[--*nobj]; + } + } +} +int main() +{ + initgraph(400, 400); + randomize(); + chartarget ct[256]; + int nobj = 0; + int t = 1000; + setfont(36, 0, "宋体"); + setcolor(WHITE); + setbkmode(TRANSPARENT); + setrendermode(RENDER_MANUAL); + for ( ; is_run(); delay_fps(60)) + { + int bnew = 0, i; + if (++t > 30) bnew = 1, t = 0; + while (kbhit()) + { + int key = getch(); + int my = -1; + if (key >= 'A' && key <= 'Z') key += 'a' - 'A'; + if (key < 'a' || key > 'z') continue; + for (i = 0; i < nobj; ++i) + { + if (ct[i].vis == 0 || ct[i].c != key) continue; + if (my < 0) my = i; + else if (ct[i].y > ct[my].y) my = i; + } + if (my >= 0) + { + ct[my].vis = 0; + } + } + movechars(&nobj, ct, bnew); + //cleardevice(); + imagefilter_blurring(NULL, 0x40, 0xD0); + for (i = 0; i < nobj; ++i) + { + drawchar(ct + i); + } + } + return 0; +} + diff --git a/cmake_template/ege_demos/getimage.jpg b/cmake_template/ege_demos/getimage.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1b5a73f7e1b5ff994f3c877b5cc882ed1e869ee3 GIT binary patch literal 3680 zcmbW3c{JPYw#R>js*svz-W*Mp7F9*i7OStfN?Vi;G=`kEh7zIC8jEzKHRaI3L0d(= z6p-$;H+G{Z;1>ipna=zlMl`cJD|?N=r&gD??-;%KzsOeF7Br0@1()kl21e zTtN(^ASP-9pa38yx$E|q;eUph_^!u2QhTMrGP?~#c|cqY1QM43{c^qA9lu)#BorhS zbxlw0QF6N`wI8E=?0!c6UfA!IpHYz?(tAx9>zmMn%WOK78~zG3m+Ea7nhiL)UH4Z5|X;6dlXN(NnOJz?LT&Zuk!C1 z`IVogVP@_umFwYyVATWWG(Gk&wErgi-+?{&f06wQ?B84zKo%sn`*B+y-W<~}W0Ga)AlFAV%4_vd=Qzq?6YU_;QRlLLAFBCgZ2nX&E|O`s57OzrA$|< zZKV==I|n{hPF*JS`u4MBT5O?}tk5jBj8hY_lC{$sN|razjqIx@oABJ@>LUfr(VF>X z!Gw-EO*7+pmi=t-A+J?;57Rznx6^>h*`<@lv)LArUb+3OtI+zIxFKx4aDaFDcB`!# z>45=J$90R*OSebyjRn&pfbSfuc5lo?m0RLDB@HRRl?}Fk(UL0m5_(5_9`r1E$URLn zl<1r>oi$rZkFN7P4ApVSY@%$f`8Y$9ko&GD2h>(SMrXK4E*IWu{=)`WH(rZ~F4%-7 zCDGc*OYv`=?;Ob=UU_jK)4H3MZ#DJkP0y|H@O|@dAs8c?*sj^k=UH#eNbN8IL^VU;mby2Ve!7Fp#s-2_Hb9Uap#d}&p9^S zcjzMb@<^0#T!#!&jG_4(o^9yH_RC2qEB*G9bGo4DWA97@IG`t< zRr=a`LoWi^NT#9n;aQi5)yiT=aBYBv8;cH_NX?B{*Z!P z6vWfjQbx$v)acS0>C~SOhTdY@{5rpb>+@mo4{GM{)!&apgx%1aQbgsEdyPGk$L*j8# zSIX?|K7UEBzkP1OBoY}O!mu?mLX~)S2@0<5j3VzRNaxOpfc8}gH^7)0SXXzYE^KA8 zljGPg0>luFY*gz``^d|>N5>=DJ6>jl1kGLOfnvHs!uOc=-^r_9#hFtdHp?XFMb~D( zWe4PkHbK7%k#$3H!;W~tf)9QRnn}yGtDwMc3a@J$PZfjilV08FNYpcTVmx@PNRUwonCtOkb+l`db zD4&ZPNP38Ha7Gqo;|^6kRa?+73Hjm@*;V$)_EYJ+5VY#6kMu-oB^->?$PCXfuC3&B zoN$I7kmD+5tXr(bM`!y7xjZYjnO%<0d!m8XKH@JMsgSUwq~aFaC=}>%zy^3KnCs!j zCN=sO-rTJ6304b_!8jC(fZxJaN}#5}8v}iymIQ^-1`J{12gUeZR$baKI^Cdi_}LGe zB_`UkC67hmTfXBu>79ET0)qx+Dy%gydvE*{>@NK0fk!*~pQzxXqN?^7g0~6(yoGVI zd75BUxQyZ(iU9G7vokCz_bDOpM9SG=^7sMVxXSl{p{(lSB-|u+{6|7FZASBjCbxX9 zuCDbIkufY#&gxr6hm5-pUn5Rw^}6OlXHMpgjM?VXx+2hUEv*uCU}aL@&G}4~ZG>}u zt_aXUpvUla&b??t3Ben>1{aebMV?u-N*=9@D1K79dWb|ViwaETpXZ)H6$C|T zCO+eJ!;mk#y#zO~U;-c9d{fc>^36*4*mhLhD940( zh85+&(&@+}+|&zUT4*4wjnq~;^=GD`TONWuT19&3%{OR7Toaqjvt~RH0W==Pw7a@4 z)25h*PlA?NE+8$JD44;GeuuVHE!0!pMZrthj>m_DKE%hQD`PbsVH;nIT0p|jhcO}` z9@kTAhtoVXD1=e1d@6kleDvpzQn_oyIJ(xB=T40X$VD!#1}%zK& zvuNsq#k{~Ap2y}XuER;#yWV+YukIAL5$5GP-~6_(8c)(WmPb42+X<^I5zvTK*+<+d zilJGVHz&oqJPQj8uAKbVDwlh>Iwrw8xcM-FM_eD7?|;wbC%~YHaodh6Gs}MAMl`l9 zOv80HZ|@Xi6Vr(~_S})*x)#?ao9DL9U+T7n$GMNa9kY;=^>Q>~(gag$f7BzKd$huM zhCW-k8m|G(IDImCa9*vtU!|e4B*KRQfAzpo$B@o3bRbH*qC`LjPfda|cdTJv&f$pr z3kyFYnsV28X7O=wv&KpPGH(s7HV;zaN#o~kA~(Ubk(XsCsEYlU7xt{Pr~DmREXtJp%3H-l6o z3`D?C%{E*F^!oG4>UnsM2_M{Wm*$c*j}$iyA`gC!c~^hvy!T?4a8rySS4^-$O@u^r zl5@=d!cjSMO80_G_@2mh5pZuf(F7s_?9^r=Mrf~A+^Roc5&@r(+fb!AyHPsJu1!XI zC4-r2mV5X4-_Qod3%6P)t@(yy*dRRMeY4K@Lkef|Jo-C~S8+^wM@0m5mb#`xC08SM zH^X=7jfUxr7o*Cek5wOo4l~p4-g+`s-q+vR3`+jS?a~~c9r-{#$_wcA9I)=o|4Oon zhWPp(F49jabWeR2RbBm40FS4i&bsmQ)>zQJzLK-Al;hud7mPQ&m?j9-nLBxAKYO)v z9+!4mIt-OoiGb=H!40>%0g??00HS=TqMr*F>d&0t4Qh**Xuw3BV1c zLaP9cKk!Lp*0?e`o3n7dkRkLQEgA>qu+KJarmt}?a( z$&W#C;B~Nvt%G?MRtE;`j?RV5A1h#S8_pL=)lJxWA?W0!2o3Z2+b$!S6QPtZj6-cE zud7thpZD1{lA5ZxOm^=jw*I|{i(xlZvr)J3q2+0{6nL145L6o#N&vBt_x;jkAYoTN zK7iO521%H&qrV894|V*t`!Fk_CnT18%+NLUs2`>>D-9H=xWLM2!@ zSRPhkoDzDptGj+YsFZkL_q6f_v0X_`Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>DDvn7+K~#8N?R^QH zRmGM5dHdS0U+){;4GqYS$R>*@2ysc=6LBIgm_%d7n8A!=T%rtr<0LUL-~=ABO2*nx5cp^?_lbKtmBfU;fdRAw>UeAqXK#fLnq&cZ{c-b{K z-1Pm&wCegyLskLGHB#3)N=McGzga^l4P+c2kJUM_EdN9-NEU>&;+0Xws_f z6Af7>5>Fa7wzc~hun+}8QI;&IaDsx7I0aaZLkyJDY@pc_#?jQWQW{@UL`JrS`b3h9 z?2YwoCygc}b#~g=UH6@aEP(E8!?z7)>mkI-F;>SoA4@PWiKLDNj6)V=B57%q?{<yR*-R#Gc#_i7U7ar1coxaN(~$MTTefUTT)KGa?Ap58iSG1ta&l~k z*w1J*uoGqg^*U}`f=r1hojm>|KCUMNOPtN&B!4JKevTbSoHX|J+iqX-lvZV*Xh;IM zeED+y*omjl3i$nZ27*DGok4NA(kUY&gR*mSsi3fkf4kWxc2JZZOBt)LlQq@F5}8|2 zNFMgkIu^pg9vpk%`Ob9O>kV(PvHGz&n#CO}g{c zMW?oEx9JmottIf@d)JN5D=51)Bh&S0>G0w!b8~Y}+qrXBVMRrSOV9Zk(lf+?N(vH* z1gCTCKsa3K^m>JO#Q9idRSjilWKni@4w)P-DjPH5=5Jhj_Al2yu{Nk(+GqM&0=Rtn z1NpP(E&BO^0~Kp3_U~V~`R%u@qeqXXVZ(~(hsrycG$ z{Zl1y>C$EH{QQw~^9x7K%gZmjxwEZ(bzObkY_@UMcs$A~$@weiv@%@_)9&57Y2l)U zRFI#``EP{&zUNbjo5Hw@j+RMDNk%mijat2(9p9*|+_zdg+;94)NFbvB4?nC}ws&v& zjlpoJI3A00li3kq$cX`A<>MzyFo0~|@LR{B316qvCurX3bLq~X|6B|ZTy@p=simcr z%gK5b$#FsLhem82;Nc{i4rjr(>`*1y9d?>J^%Tm_&!;!ucvCBLL>7+h9U-aa zTrXGE+1Z(M@ZiA<9(w5U7ao6njYlieC;Cb=kxQ5UM}Bo}{f4Tl>K}6qCx)EhIiac& zG}(B8$CnU5K#;uV!8LU7;OBJO+&MIIR2iR6CCd)_&n5JbPAsnER+|&6I{QMoK7c=9XF0nKmByJtC3M3Za?rxakD9WiIZAMMq;V0YM+s6PD$cG-_X{v{Rm7FL^1W5pu9n@G9l=gD%BWtOssEC$aaRmq4 zEqoj`9gzvtgz($ZYw833xupr4+TWBypXtXDMX0A7Fn z9rMfg?fY|ETbmmr!cbR8js!#lj6nfV00kq)kRafMV@hoywMGmA@D>;7__{OCJd^HP zeiuEpemyZ9iof4j?&T+TDWilO*!Qh%FfE7bk3PECxSq`t-YO^8k?w^vlYHi zb8%70GFpaHmH!v@3KZ}0ix-}mhKU0X}51$HOKiM>!SI19Fa2vwxYNdf|yi$OHS zZv@&WPo6|$P8v<+<>gdeRZRtjd35zPSJS!YoI?c#1!Ojx$m8))#lHQtapOjo)~D#L zw>DFKeS_#s(3DfARE2!?cWl|R1r_&-1{^?E*`g^^PJ8f^ojZRJ3L6ex?*4eP67F?P-||U+_Hp13VB;tkVjKaKA9z{oq|Ch zU3cBJG;_vua-}&b5{?Q`-{eqd-MYtU$BrFbCInGtsWu23h&vY)4BNSC<$v{QigUmK zyz$26uEJqsuQ+gE|C1c!ord`b%-3QFJrn@&>#SVT~m^E1U!S7 zVUvyko|@HJD=S!lprexFBAPSnRB~EPGb_TVkx|ZJAvXxfde?R@5(?2C~Nw$OWc#_RWj5`)noiZd!cc*Ku=siJ(`%O!i z-ZXD-`Tn;&?X5;g7-q|4E-UR8|IcmEdsyoJb^G>B&)^zXSKXK~Gr!=hU^vF~6dm!j zOe}Q(PXKnzqoUzhzL8-y(A~>^NWqqRvghWJyKFqIf8lTR%B!ynkl=s-AYZ`Biuqz` zLU7E;j*7*QQ^Kn^-+9N6&(x~!6%B|4RvkY4gO=t-Bb)$&TBsd_OQ<#Zw z;DK0zlB3E-RYNjBuz@1*)l?mXnKOhiqAvaiHWGv_?t_jxX>=J?R(?TkUJq^CwvC?q z)1T?(S2hXhg4)Xqal#Pdpz-&YzC06iTY!?j(0XxJ91V&ch}7A`#V z$y;yz;TrKHg-jOfX0>7f*uXA9GH@*zzwv;IQ3#R;r-JLO?4`eX@kKO&1LQ*;etKu; zr}Xk`Z;BNP=mY^euH$OezBtYxqLY-H{Q zyciQ_N0HHJ{;O<|46KC>1ak*SFDb7_T~dM&4#fl~x#2}c1ynL(IAs(Mqkz#yl`UQh zg!mlHMpu3f*c77=ByfUR3gc`Kw|}_3&qi=S05lqmzlBu9@IaahqT$M;ax8fB=FOX( zTJbJgu;BDjD5uIR@BpL&4+QB!y#Nksi-rTB@#Dsl%jFbKp{b>v%R${W$>(brvPi!ejE08#oM)fi^eb8V z2)*#a3-uxU) zKbXGrogd8SMweO4oNaIl2B}jZD)5plW$@+*z=d-X)8{ymq*0@W)8t7L_*|6Udv6=n z)i&^9mN-#H-cyn$b^K2%%QI*`B;WX|IM4?gFaSZh`MGDYVc&y?QcVC9kSK1b{pG*C z`0Cub=TBI<^3kc))iw7^i9_rsZ6EfP*M;iABw%?OK!W#_s)tUA)9l$CJUeV61KGCi zeZCIT#COBB+JCAe6$$Y87>>)d5b0x_GBdO5wIaQy0R`~ww_o(knLA?T#%?wRjH9U&4tAHA$JPqk4KFf`Sk!0^yHHdHcgy3X@S{d ztxG1f88c=`p~Ye*yWK|ZZLMq{C1_Av6)Y%;TO0%97;6a&C_lr44?XnjKF^H|IDnv+UV5T-{``eA z(p;`8s0e~p5wNmE04zwO+#+p5ip9+mp$C`K0EEp$V2Ck z@<^CN4`I6?iF``|h}KX6mgFPS#t+5~6axtCm6dt}oDm~RID8o+%5Qz|ee(GOqHci` z{^K37kAK8%;Eg3o=p-RRNK13SQB$+`pmwNFG-v>V?!W(z##Q&P`qupU^WTQ%h!BSx zD^!%7L!6xh?3;+|SW1MGK*O0-Pz@=?7C>@rLKmEOo{-?Wx_Wx)udgt$GLM$PNd}bv zKmtAfM|2)hVNp@@4}bWBJH(HE(4YbcI^&F!4?Xw%!wV)%oN%$#YNHUA;s%41>2^~D zqcuT41IGXgRhLqjDm6lKH3_v@Eg1ad(+S$W`|srUhZqlpK2k!(e<>g-0;o75jsjrj z5hKbr{Oo7f@4%t{(4YgT=(X22ym-YG-ye~klT+!iJ803u1vGo+3@R=xpd@D($yk_l z>^O8B^Cjal+lce=2-Y<-=veDKZ`P?)T9i*gzn?bz@i_(#(g|B?;#krwhW~}cvU*Cp znMyLW!jys2nMUWFbM8^gI~e3XVR7tT29)qn+z=M2BXs+J+%u)QrFo}2%|U5S2RGbE zO-)TyaiD?@R~@Dx0|p@x7x6YG>|~M*tm=#A&7=!1KA$?nFA{?fjR56+i``bUE*fyaV(#lQUMg3iv)brZ*rp@82fRMKiS z)39NMH1D*z^o>OeY5dr+l+M5!85JFaXXKWf*#}k1i z0D#pBEzQo%qM01yEn2vcX3m&F1^IboV(|2wp4qG>%F4>1k3Ra4D)#Lqcy?qcB39%& z7#)cqvjkMej>6M3BYq4=qBLsc$oh*ex$t4_vHDGY9|-FOtz5az-PY8+Zg^>NZb3nw z=(Vg2w`e$qrG%5fXbu8mZahYCGTa%Io104+8R;~vsE|f;?t9v4r_$zk-y=s_7CCHo ziiX1iAThuIiDL$da0p;xU;LXCtGQh18JsE2S$gx$*Y064mI{5hi@S{KGnd`R|4SJv6Gjv9h+Vs}dI+2jv!kjF&FKI$*X>k@l#AOE+GB+h0nC6_@fSvk4yRae&r>}k$zk3F`kU(48zM}rRFs{0f^;B|J}mRV7QaD|Kq+{1V;nlg%d$p4fsyK&p2(`X|q|6u=D|l7_Ya^V}`$82-c{ zxB&nxl$zvG_6iag2w=exhPeQg41_U4jPD-A7!)C`Nd_7t#{iu{!NfSg#rbeBAdbNa z0WdI}4A%Z(T@IFOB;$#Gy@`1uBwI891n^9{w^p{bHs6Tg7y|)9f*2S`j0}|7)8e;i zY=loE%!SlpHjjU64nnFflbD^;YpMzFE2lo;t)cQ#jYSs;hCM~SxR=>tBa6*N8M(P{ z=0wxZyJp2TAyhfgG++{FGQRoCw)U1A@yU@SON;=8fdb>Vlrn}wvvVSi6B5Uu#sMBw zSz|sfqz%{caTsbNUm&c3kk~lyL%OAeD`0IlD_&tGH)lDRZ_(@c`bco z_9ZZg_YfdszF1@#sFCYJ-H=Ym03YU-VDbtpSuwXF6F9Tk!k{|HU~y1fX9`W6IR5kt zzP;!padLpD-vF*(|H{&!uj2uIGH#EAL&T0l04VZXfC-7=EgB^N6spSSAbkv`TyzP5 zS=fXGN&tZrmj41Q>4D|E2fGsqBtQmm05HMo%fB(WD(6}_wBhguA&$vpVE}CuiR;O1 zv^Exx9`Vg9ue@NuJafOom`5MocxE)bF=rs*8HLw`iYC;&E(6NHh4IIE zs5<}#TAH(vDk?`)_y=!U(zpazO0Gan>v>!Uq~(H1j4k3den>7#!A8mfR&FR?z#uzY z+dRfC@4U8ATs{EQXB+tRt2@Sq!v1F&C^s?-u_6@`C_vD{5@y&fNS-Vc;4(o5ImE~C ztru2dwtr%uLB)DMgfddJ7)0zd0ZXK97+8f}gqe`1B#eLy;S%nJ!dSqL0ss>AVEzT2 zsMo=PA-5hl>JxzLHt%!?{Qf_M0)d>6uVL*)ma1ex3=S zX~5yPa3lz%HPvS+RCWxxGhfL6!>K@G0VJdq<)Fq4Y8OL7xvT&bxiZ#gBSyq?NT4~> zX%`g_5cLVbj>8St_-kv%>k%@rDgrzi&;|t<7zD+77fg-^e4WHWHL-)?20@iMZe!pQ z9OFrw2T5ZIOLFscY`bjfvGlC`*8GCfs@$AnpTT4kjR)bCRe*r;KfZ$d1i%7NbPmh~ zGxix+v9Dz}U24y2ZnhW)G{uuT&1veQlYE7(VzV?ViYG`MlI38S^1{ zhO}Xl2yq*}EF`2X84*Cpn2%M@VD)5VWp6JXKH@JfcivwXFI`+KZpO6pt&c9;wda%D zKKpd%sfmc6uVd*G(gv2Qjk}7~sLW!b3EmjCkMn1}0Snem;)4_>tBp*yG|I}&UvSN} zm%c6U&_C)mfWKSwmwz_~J-^k3e8Qe#mJ0wvbzsO*p^W3$?lFkbu%E%?lq;4H<8;C+ zGkCZrEj_E+?sPqnlUw}6O?TYX?fR5$uWdVH+t#=4t2$IMO;5065P8h)7 zt=*KV54Aq63kS~Du?-WU3L9u?@TF0Q)Je6)D6@1BP+c)O4wchUV@6ii?{kNjtX;Nj znOsuU2YT<-cfRx1YcD_7(pb$v$YTIc0L5Db(>k45E}+yT6E0c|iE(fZNn~BFz5T9b z!{pU{rxOBj+47a{{Pc{6GHvEd*&9$O;1#1CP(v}A!Yn;0jJnKg@fsEkI9iB=%VM=| zcBVV7T5<2aHCna(qR&6sH~qN{Pi?HPJtP)wLJAo@z7vDUpbmP99$7fD&&tlC%*+gOxzfmF zmMa28>cuS8S~`qiFzgA(qBo2$n|k3ufd?8nb?lA>i_V{vH*91jhlkur{GV`2(l&O5 zHP8rlbO^7)tT7n5#?q1~o1l0k^b4)JKGO+F;Qdd$x{q2L#>9eNYG`N>qbXJ^gT`RV zheSvrLkWCxF3KSbj)xND@kb-yedf7mHfdE2IPKWHBm2caKl8e$wPA{QVH;}9%#omw zI+4~f8<5tq`a)))5m;VgvRWxUujuJ(zW41VvbNq+j{q)Tp498J{#dJPZOnIAj0{kK z>T0Ta>*vHFBJ)&sg`BDKK3-|%41hUo)O#WZ%jN5zc=#2qszD=w`26!vZw>le#vo=# z434EPa4b+^0BE&2gdN7KUOEg+Abetg$4;2idewE8WouRSntD{>oSEB4rCDwHkT&F} znVFe1YSd`nX7Phzyb{WTJcB68FlAw1`LL*kgNJ8+q9L_!_#8j@$QZit*TyAj{wHnd}o>2mf^!* z`1lQ8W==~_7a#X>XJpB+fDJPSt~x?7YKkQOYAwu|cHgh=?$ayueWewvR(QrunDpKD z4tz@xW(8f!kpWB#?~92iepai+%6*QP4!N_#5%LGZj>@XWK7YxmM*x$dKsLe_Jb=w+ z6AyHtqJks8gU4G3) z;kYT>*y88RM~+a3!x-uhi#cYW&nMadCz$Sbv)M7x&Rx5gKC|wXuGiIiLp|ES)lagwRI3d(2+iz!8Imr7#c-Q-DD&DlVat(h-!Ile=Z!8FMZxE6eLueA=S~CSu7< zIb3cNA8i1`{uSisi|<%LQO^F>1^;=)#aFMDSN!v7(V|I>bLX6~Bpj2T*v!t)Znu-w zZWEc1&mZKt)JDa_ipgd%b669gef##E@#dTF_WE0bJp#yqN+w1%FeHRv6%*KIzG<7M zotl~&MO1&Fy4iih4U2u+C0`4=;ijv~$BdtNTSsS*8XB9$C!#vNeopU<;v*E8z(*1Q zyAB&!kef?pBkOZ7sYd|g;c%J=N?BdyoNQdUf4o*NLj(?>jmEB5xV}zw&u?!3)y4mE z#kn)*Eo_c)PK=BMs*G_&NMwF~fru({b8>`+>5@8kbCthPyZnTxM*xkKOjr3PY#sna zehY&vh-+|Xxcc=K$ibw`ue@;6rI&wqk}EUorErub&Y8x5<%y3mLJ%UFc3}#F?YnK? z!2{=Lm!1%H3*aBtzm!4ANR}9$=aP^%Ow>S)5zZKqoU=h5S5iD+D?SH<;273D`9J56 z8$a%?VMRq^`V*r?cxgwbdKhMt;m*GD{U>Xeoe*^kpdsuVp^IZA8LI)1-%1sh^AmBA zc^LJ0<2&5^nROec(h8HnH4!aI>B+L?kL!`kBipFT6 z*f{~}7C;c}0)`7P+Kjjfy~cNvqR;=~71p6d+~M0f84)8$P+_zMTQRf88YMzDO3u)V z_ek9XXf~sMp|0p5keH}}I&<$~dXaCwfAQkQ{rVpLS4lZWV~1X6>`=f7z<4X2qa1u- zp05?}k-7y?4ORg=tO|hlh3&u}Il%}oymKJn|BEa=G-&Zpe;QrEu!{ZAJNRTBJ~IO1KBRag@uK)Kgopup4_KiE;_FBp%E;Gi zF`A7Q>y5E!a3o6fL_GqC`@rsrsY~uLtJy*pHX_I;U1@0_|9bWBcFU4Ohd6+K1KI>r zgoFU!-ZzOwNq_^aqd+7)MHKFVx&;u!R`614lE}5!yW*RYkc)ven z{m&qd2V>E2;P+@0revYC;3LFNGMf0N_M=z+%C7S}}VkQ#%jsxHl)SJq0T2@-2`=8inEUqcpoh*D@$Y`;;+OoSvV$ir8zg8efHT{ zA~j^`Co5KTW@Tr;2nob%5fBpj!3ws4!4N*u-y?MovrabyYny}ktAOI;Imj8~;_Kg> zi9D;78q!pdn{`V>tZ$TqCJ+?-Q9o{{bHj!W|M1PjW2svJJ32h~;!C`rA3Pww$K2T1 zLfhW|kQ(Y6_(tYkTCpKb=Py}ufJ2!>p^#s!FvDMq#e#XP_)jO(7_HcGsapVVx$Cw^ z+dS>%&;RKK`e?_;v}@09@&^NKU=1y|{O2uQ%W#iFs2j!vqryn5t;{xN{7G8$tcw{V z*%{1P%vhhIwqS%2VC1AQw)xDMyh>#p!+DfPJ!#CSANRsNyVo#{67lGtjI6p45|}Ax znjB!SZKxX|cDkV+Z}8EMoeh~;xttwZ*(-C(V|C&^v<{93MK;NVhgE=}5oin=r5cY$ zBC5w{HcOeY0sxHlC^&|53JkRc12nLzO97n9>R##|s2`11_ka_`F_ZzgSf2F{l$mC| z2VYS-eE2ZEvvn(Ndi6DW?1`tSt;K_DyQLlh+CGlq;ZNMX^_>vE;XaT++!Me7Kmd#_7|+x-Y=6^=6`s!zRvfNxs1ptZ zA2c(Vt<=!cGDj6K`?ArEbR}T1x`~G{D*Xu84>qA?d6FaQUz zAO(;VfC|8H9uKJ)_fn06S}WBCWB=H)15KzGz(G@>HF)N(XPq)-!X{l33+44<8L-2d zPKOR1K9gg@ZUgG09(4DXEh9pDhm2L2%F7R&iE%)zzQUItZFa_sZ(y-H^5&g-=IvYr z4*@U%oSPXBs2JyzdOCxU9sh4?!=PibncdyGRHG+s3Z(ul?aFl+&c0tox5jPr^Cl9C9N z1yE2uFdB%haysZJT#s#PIdv|T73g5p1Msm`*PS>nQUyPun-9! zIg5lt-ikx}kNd5eE(x49YgXsPQ%`Nuep2lhwbwCt^VoYJ9weXFCzkgj-N68>-egHH zD?JI5(CB@11VDpRfb;;!V^tI7;CPU9Vnh~s6Sj(RT&Xy&MZ-{!YP4bn7(f8{%0nwH zPz*p*fmIrz7}@|niFw?Z@e?dId<@NG7HbIc{!5s%lctu|8~OZk=_nOeo^{#MWp3_) z+d~nV8zOiP`a7t(v5w=UR61V|jT4T}fFYZ@4pf%XOc zE5EnodyrJrc@%ZoKmd2iCEr=m($@NLO>O;b{23jLl>tEUVqk*z1&~-m8>FKAJj&0{ zCA-tsytjNW#s!sePgUDgX+Q%8n{W?xKgD1q$E%Q2v*0ig_J7BZ^ab+A*)eH5-v!oJ+tr})bnf7ykAUG{Mr;vOB?ZrrzM7=&iCeD#93~X+84m%j@ z9nWoi;WvDZkYdp@#d(G6TU$HERMpf_r>~O&!64fKKTDsS`om*sbHIY}Eb2uXR&STX z>G*&p7VqJz2UYe-u~KWL;>wmOdxotlt3Xf^1gTNdq8I@(7^VYToL8(QAB=}Z9ncQg zzheQ0O|E2)6c-LVYs!>U@~2Nfl|J0QT}UC`VJ5r-!;IlBf54CLd@Y; ztf>{^LEx=GxH-HkME!UK?}mp%BfyGOgTaGMcz!$so)=sE#(rwMRHuSfrzyt-bxeH$ z8|OOZ?mrNmAqJ4q$@nXPf>LjUo`o7J0swILIF5akO)ZZeRlq5UKqFE~HB2=wwT{%f zjz=d_MiBry))b(z*n)5_m5v1u=K&HlLXoNmkps%3K137P3Oe=yAyvmplQOuVQ~&@9 z08j04T#K?erU(zFR4MiPR5~8WARJHKc1Nngib1Kr0S>|;0Ef^@9S0zgJV;<_ImKOo z9ZO%<7)ac)4+~%*VE|M$6gL9Fz5p=X1dN+wi*h)wu2Gx{?w64&hkK;%gRn(?*rs;e z@kl)*2+su-gfuB>1YnS;RP_X>Ue_)4^Z+MHkW#2L8hax2Wgw;gR?nsOQ@J}r!xaN0 z00h7Q3iffmIu1a)!jZmGPY={vQj&VC8V?o$3 +#include +#include + +const double rotatingSpeed = -0.03; ///< 旋转速度(单位:弧度/秒) +const double fullCircleRatation = PI * 2; ///< 完整圆周角度(单位:弧度) +const double starAngle = PI * 4 / 5; ///< 五角星角度 + +/** + @brief 绘制五角星 + @param x 中心点 x 坐标 + @param y 中心点 y 坐标 + @param r 半径 + @param a 旋转角度 + */ +void paintstar(double x, double y, double r, double a) +{ + int pt[10]; + for (int n = 0; n < 5; ++n) + { + pt[n * 2] = (int)(-cos(starAngle * n + a) * r + x); + pt[n * 2 + 1] = (int)(sin(starAngle * n + a) * r + y); + } + fillpoly(5, pt); +} +int main() +{ + initgraph(640, 480); // 初始化绘图窗口 + setrendermode(RENDER_MANUAL); + double r = 0; + for (; is_run(); delay_fps(60)) + { + r += rotatingSpeed; + if (r > fullCircleRatation) + r -= fullCircleRatation; + + cleardevice(); // 清空屏幕 + setcolor(EGERGB(0xff, 0xff, 0xff)); // 设置绘制颜色 + setfillcolor(EGERGB(0, 0, 0xff)); + paintstar(300, 200, 100, r); // 绘制五角星 + } + return 0; +} diff --git a/cmake_template/ege_demos/graph_alpha.cpp b/cmake_template/ege_demos/graph_alpha.cpp new file mode 100644 index 0000000..383533b --- /dev/null +++ b/cmake_template/ege_demos/graph_alpha.cpp @@ -0,0 +1,46 @@ +#include +#include + +int main() +{ + /// 初始化绘图窗口 + initgraph(640, 480); + + // @note Step 1: 绘制背景 + setbkcolor(WHITE); + setcolor(BLUE); + setfillcolor(BLUE); + bar(100, 100, 600, 400); + + // @note Step 2: 创建一个PIMAGE对象并设置背景透明度 + PIMAGE img = newimage(640, 480); + setbkcolor(EGERGBA(0, 0, 0, 0), img); + + // @note Step 3: 绘制形状并设置透明度为255(不透明) + setfillcolor(EGEACOLOR(255, RED), img); + setcolor(EGEACOLOR(255, RED), img); + setlinestyle(CENTER_LINE, 0, 1, img); + setlinewidth(10, img); + ege_line(100, 100, 400, 400, img); + // 改变颜色 + setcolor(EGEACOLOR(255, GREEN), img); + setlinestyle(DOTTED_LINE, 0, 1, img); + setlinewidth(5, img); + ege_ellipse(200, 100, 200, 200, img); + // 设置填充颜色 + setfillcolor(EGEACOLOR(255, LIGHTMAGENTA), img); + ege_fillellipse(10, 10, 50, 50, img); + + setfillcolor(EGEACOLOR(255, LIGHTBLUE), img); + ege_fillellipse(100, 10, 50, 50, img); + + // 将图像绘制到目标图像上(为NULL则绘制到屏幕),保留透明度信息 + putimage_withalpha(NULL, img, 0, 0); + + // @note Step 4: 清除图像对象 + delimage(img); + + // @note 暂停程序,等待用户按下任意键继续 + getch(); + return 0; +} diff --git a/cmake_template/ege_demos/graph_arrow.cpp b/cmake_template/ege_demos/graph_arrow.cpp new file mode 100644 index 0000000..cdff842 --- /dev/null +++ b/cmake_template/ege_demos/graph_arrow.cpp @@ -0,0 +1,42 @@ +/** + + @file egearrow.cpp + @brief 画箭头算法演示小程序 + */ +#include +#include + +/** + @brief 绘制箭头 + @param sx 起始点 x 坐标 + @param sy 起始点 y 坐标 + @param ex 终点 x 坐标 + @param ey 终点 y 坐标 + @param r 旋转角度(弧度) + @param len 箭头长度 + */ +void draw_arrow(float sx, float sy, float ex, float ey, float r, float len) { + float c = cos(r), s = sin(r); + float dx = sx - ex, dy = sy - ey; + ege_line(sx, sy, ex, ey); + ege_point points[3] = { + ex, ey, + len * (dx * c + dy * s) + ex, + len * (-dx * s + dy * c) + ey, + len * (dx * c - dy * s) + ex, + len * (dx * s + dy * c) + ey + }; + ege_fillpoly(3, points); +} + +int main( void ) { + initgraph(640, 480); + ege_enable_aa(true); + setcolor(EGEARGB(0xff, 0xff, 0xff, 0xff)); + setfillcolor(EGEARGB(0xff, 0xff, 0x0, 0xff)); + setlinewidth(2.0f); + draw_arrow(100.0f, 100.0f, 300.0f, 150.0f, (float)(PI/8), 0.2f); + getch(); + return 0; +} + diff --git a/cmake_template/ege_demos/graph_astar_pathfinding.cpp b/cmake_template/ege_demos/graph_astar_pathfinding.cpp new file mode 100644 index 0000000..607ee49 --- /dev/null +++ b/cmake_template/ege_demos/graph_astar_pathfinding.cpp @@ -0,0 +1,959 @@ +/** + * @file graph_astar_pathfinding.cpp + * @author wysaid (this@xege.org) + * @brief A* 寻路算法可视化演示 + * @version 0.1 + * @date 2025-11-27 + * + * A* 是一种经典的启发式搜索算法,广泛用于游戏开发和机器人路径规划。 + * 本程序通过动画演示 A* 算法的搜索过程,包括: + * 1. 从起点开始,评估周围节点 + * 2. 选择 f(n) = g(n) + h(n) 最小的节点扩展 + * 3. 重复直到找到终点或确定无解 + * 4. 回溯找到最短路径 + * + * 用户可以通过鼠标绘制障碍物,设置起点和终点 + */ + +#ifndef NOMINMAX +#define NOMINMAX 1 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 文本本地化宏定义 +#ifdef _MSC_VER +// MSVC编译器使用中文文案 +#define TEXT_WINDOW_TITLE "A* 寻路算法可视化演示" +#define TEXT_CONTROLS_TITLE "按键说明:" +#define TEXT_CONTROLS_START "S/空格/回车 - 开始/单步执行" +#define TEXT_CONTROLS_RESET "R - 重置路径 (保留障碍物)" +#define TEXT_CONTROLS_CLEAR "C - 清除所有障碍物" +#define TEXT_CONTROLS_GENERATE "G - 生成随机迷宫" +#define TEXT_CONTROLS_AUTO "A - 自动演示模式" +#define TEXT_CONTROLS_SPEED "↑/↓ - 调整动画速度" +#define TEXT_CONTROLS_DIAGONAL "D - 切换对角线移动" +#define TEXT_CONTROLS_EXIT "ESC - 退出程序" +#define TEXT_MOUSE_HINT "鼠标操作:" +#define TEXT_MOUSE_LEFT "左键 - 绘制/清除障碍物" +#define TEXT_MOUSE_RIGHT "右键 - 设置起点(绿)/终点(红)" +#define TEXT_STATUS_READY "状态: 准备就绪" +#define TEXT_STATUS_SEARCHING "状态: 搜索中..." +#define TEXT_STATUS_FOUND "状态: 找到路径!" +#define TEXT_STATUS_NO_PATH "状态: 无法到达!" +#define TEXT_STATUS_AUTO "状态: 自动演示中..." +#define TEXT_GRID_SIZE "网格大小: %d x %d" +#define TEXT_ANIMATION_SPEED "动画速度: %d ms" +#define TEXT_PATH_LENGTH "路径长度: %d" +#define TEXT_NODES_EXPLORED "已探索节点: %d" +#define TEXT_DIAGONAL_ON "对角线移动: 开启" +#define TEXT_DIAGONAL_OFF "对角线移动: 关闭" +#define TEXT_FONT_NAME "宋体" +#define TEXT_LEGEND_TITLE "图例:" +#define TEXT_LEGEND_START "起点" +#define TEXT_LEGEND_END "终点" +#define TEXT_LEGEND_WALL "障碍物" +#define TEXT_LEGEND_OPEN "待探索" +#define TEXT_LEGEND_CLOSED "已探索" +#define TEXT_LEGEND_PATH "最短路径" +#else +// 非MSVC编译器使用英文文案 +#define TEXT_WINDOW_TITLE "A* Pathfinding Visualization" +#define TEXT_CONTROLS_TITLE "Controls:" +#define TEXT_CONTROLS_START "S/Space/Enter - Start/Step" +#define TEXT_CONTROLS_RESET "R - Reset Path (Keep Walls)" +#define TEXT_CONTROLS_CLEAR "C - Clear All Walls" +#define TEXT_CONTROLS_GENERATE "G - Generate Random Maze" +#define TEXT_CONTROLS_AUTO "A - Auto Demo Mode" +#define TEXT_CONTROLS_SPEED "Up/Down - Adjust Speed" +#define TEXT_CONTROLS_DIAGONAL "D - Toggle Diagonal Move" +#define TEXT_CONTROLS_EXIT "ESC - Exit Program" +#define TEXT_MOUSE_HINT "Mouse:" +#define TEXT_MOUSE_LEFT "Left - Draw/Erase Walls" +#define TEXT_MOUSE_RIGHT "Right - Set Start(G)/End(R)" +#define TEXT_STATUS_READY "Status: Ready" +#define TEXT_STATUS_SEARCHING "Status: Searching..." +#define TEXT_STATUS_FOUND "Status: Path Found!" +#define TEXT_STATUS_NO_PATH "Status: No Path!" +#define TEXT_STATUS_AUTO "Status: Auto Demo..." +#define TEXT_GRID_SIZE "Grid: %d x %d" +#define TEXT_ANIMATION_SPEED "Speed: %d ms" +#define TEXT_PATH_LENGTH "Path Length: %d" +#define TEXT_NODES_EXPLORED "Nodes Explored: %d" +#define TEXT_DIAGONAL_ON "Diagonal: ON" +#define TEXT_DIAGONAL_OFF "Diagonal: OFF" +#define TEXT_FONT_NAME "Arial" +#define TEXT_LEGEND_TITLE "Legend:" +#define TEXT_LEGEND_START "Start" +#define TEXT_LEGEND_END "End" +#define TEXT_LEGEND_WALL "Wall" +#define TEXT_LEGEND_OPEN "Open" +#define TEXT_LEGEND_CLOSED "Closed" +#define TEXT_LEGEND_PATH "Path" +#endif + +// 窗口和网格参数 +const int WINDOW_WIDTH = 1200; +const int WINDOW_HEIGHT = 800; +const int PANEL_WIDTH = 250; // 右侧控制面板宽度 +const int CANVAS_WIDTH = WINDOW_WIDTH - PANEL_WIDTH; // 绘图区域宽度 +const int CANVAS_HEIGHT = WINDOW_HEIGHT; // 绘图区域高度 +const int CELL_SIZE = 20; // 每个格子的大小 +const int GRID_COLS = CANVAS_WIDTH / CELL_SIZE; // 网格列数 +const int GRID_ROWS = CANVAS_HEIGHT / CELL_SIZE; // 网格行数 + +// 颜色定义 (使用 ASTAR_ 前缀避免与 Windows 宏冲突) +const color_t ASTAR_COLOR_BG = EGERGB(30, 30, 40); +const color_t ASTAR_COLOR_GRID = EGERGB(50, 50, 60); +const color_t ASTAR_COLOR_WALL = EGERGB(60, 60, 80); +const color_t ASTAR_COLOR_START = EGERGB(46, 204, 113); // 绿色 +const color_t ASTAR_COLOR_END = EGERGB(231, 76, 60); // 红色 +const color_t ASTAR_COLOR_OPEN = EGERGB(52, 152, 219); // 蓝色 (待探索) +const color_t ASTAR_COLOR_CLOSED = EGERGB(155, 89, 182); // 紫色 (已探索) +const color_t ASTAR_COLOR_PATH = EGERGB(241, 196, 15); // 黄色 (路径) +const color_t ASTAR_COLOR_CURRENT = EGERGB(230, 126, 34); // 橙色 (当前节点) + +/** + * @enum CellType + * @brief 单元格类型 + */ +enum CellType +{ + CELL_EMPTY, // 空地 + CELL_WALL, // 障碍物 + CELL_START, // 起点 + CELL_END // 终点 +}; + +/** + * @enum CellState + * @brief 单元格状态 (搜索过程中) + */ +enum CellState +{ + STATE_NONE, // 未访问 + STATE_OPEN, // 在开放列表中 + STATE_CLOSED, // 在关闭列表中 + STATE_PATH // 在最终路径上 +}; + +/** + * @enum AlgorithmState + * @brief 算法状态 + */ +enum AlgorithmState +{ + ALG_READY, // 准备就绪 + ALG_SEARCHING, // 搜索中 + ALG_FOUND, // 找到路径 + ALG_NO_PATH, // 无解 + ALG_AUTO // 自动演示 +}; + +/** + * @struct Node + * @brief A* 算法的节点结构 + */ +struct Node +{ + int row, col; // 位置 + float g; // 从起点到当前节点的实际代价 + float h; // 从当前节点到终点的启发式估计代价 + float f; // f = g + h + int parentRow; // 父节点行 + int parentCol; // 父节点列 + + Node(int r = 0, int c = 0) : row(r), col(c), g(0), h(0), f(0), parentRow(-1), parentCol(-1) {} + + // 用于优先队列比较 (f 值小的优先) + bool operator>(const Node& other) const { return f > other.f; } +}; + +/** + * @class AStarVisualizer + * @brief A* 寻路算法可视化类 + */ +class AStarVisualizer +{ +public: + AStarVisualizer() + : m_startRow(GRID_ROWS / 2) + , m_startCol(3) + , m_endRow(GRID_ROWS / 2) + , m_endCol(GRID_COLS - 4) + , m_state(ALG_READY) + , m_animationSpeed(30) + , m_autoMode(false) + , m_allowDiagonal(true) + , m_pathLength(0) + , m_nodesExplored(0) + , m_settingStart(true) + { + // 初始化网格 + m_grid.resize(GRID_ROWS, std::vector(GRID_COLS, CELL_EMPTY)); + m_cellState.resize(GRID_ROWS, std::vector(GRID_COLS, STATE_NONE)); + m_nodeInfo.resize(GRID_ROWS, std::vector(GRID_COLS)); + + // 设置起点和终点 + m_grid[m_startRow][m_startCol] = CELL_START; + m_grid[m_endRow][m_endCol] = CELL_END; + + // 初始化节点信息 + for (int r = 0; r < GRID_ROWS; ++r) { + for (int c = 0; c < GRID_COLS; ++c) { + m_nodeInfo[r][c] = Node(r, c); + } + } + } + + // 重置搜索状态 (保留障碍物) + void resetSearch() + { + m_state = ALG_READY; + m_pathLength = 0; + m_nodesExplored = 0; + m_autoMode = false; + + // 清空优先队列 + while (!m_openSet.empty()) m_openSet.pop(); + m_openSetLookup.clear(); + + // 重置单元格状态 + for (int r = 0; r < GRID_ROWS; ++r) { + for (int c = 0; c < GRID_COLS; ++c) { + m_cellState[r][c] = STATE_NONE; + m_nodeInfo[r][c].g = 0; + m_nodeInfo[r][c].h = 0; + m_nodeInfo[r][c].f = 0; + m_nodeInfo[r][c].parentRow = -1; + m_nodeInfo[r][c].parentCol = -1; + } + } + } + + // 清除所有障碍物 + void clearWalls() + { + for (int r = 0; r < GRID_ROWS; ++r) { + for (int c = 0; c < GRID_COLS; ++c) { + if (m_grid[r][c] == CELL_WALL) { + m_grid[r][c] = CELL_EMPTY; + } + } + } + resetSearch(); + } + + // 生成随机迷宫 (使用递归分割法) + void generateMaze() + { + clearWalls(); + + // 简单随机障碍物生成 + srand(static_cast(time(nullptr))); + + // 生成一些随机墙壁 + int wallCount = (GRID_ROWS * GRID_COLS) / 4; + for (int i = 0; i < wallCount; ++i) { + int r = rand() % GRID_ROWS; + int c = rand() % GRID_COLS; + + // 不覆盖起点和终点 + if (m_grid[r][c] == CELL_EMPTY) { + m_grid[r][c] = CELL_WALL; + } + } + + // 添加一些连续的墙壁使其更像迷宫 + int lineCount = 15; + for (int i = 0; i < lineCount; ++i) { + int r = rand() % GRID_ROWS; + int c = rand() % GRID_COLS; + int length = rand() % 15 + 5; + bool horizontal = rand() % 2; + + for (int j = 0; j < length; ++j) { + int nr = horizontal ? r : r + j; + int nc = horizontal ? c + j : c; + + if (nr >= 0 && nr < GRID_ROWS && nc >= 0 && nc < GRID_COLS) { + if (m_grid[nr][nc] == CELL_EMPTY) { + m_grid[nr][nc] = CELL_WALL; + } + } + } + } + + // 确保起点和终点周围有一定空间 + clearAroundPoint(m_startRow, m_startCol, 2); + clearAroundPoint(m_endRow, m_endCol, 2); + + resetSearch(); + } + + // 清除某点周围的障碍物 + void clearAroundPoint(int row, int col, int radius) + { + for (int dr = -radius; dr <= radius; ++dr) { + for (int dc = -radius; dc <= radius; ++dc) { + int nr = row + dr; + int nc = col + dc; + if (nr >= 0 && nr < GRID_ROWS && nc >= 0 && nc < GRID_COLS) { + if (m_grid[nr][nc] == CELL_WALL) { + m_grid[nr][nc] = CELL_EMPTY; + } + } + } + } + } + + // 初始化 A* 搜索 + void initSearch() + { + resetSearch(); + m_state = ALG_SEARCHING; + + // 将起点加入开放列表 + Node& startNode = m_nodeInfo[m_startRow][m_startCol]; + startNode.g = 0; + startNode.h = heuristic(m_startRow, m_startCol, m_endRow, m_endCol); + startNode.f = startNode.g + startNode.h; + + m_openSet.push(startNode); + m_openSetLookup.insert(m_startRow * GRID_COLS + m_startCol); + m_cellState[m_startRow][m_startCol] = STATE_OPEN; + } + + // 执行一步 A* 搜索 + bool stepSearch() + { + if (m_state != ALG_SEARCHING && m_state != ALG_AUTO) { + if (m_state == ALG_READY) { + initSearch(); + } + return false; + } + + if (m_openSet.empty()) { + m_state = ALG_NO_PATH; + return true; + } + + // 取出 f 值最小的节点 + Node current = m_openSet.top(); + m_openSet.pop(); + m_openSetLookup.erase(current.row * GRID_COLS + current.col); + + int row = current.row; + int col = current.col; + + // 标记为已探索 + m_cellState[row][col] = STATE_CLOSED; + ++m_nodesExplored; + + // 检查是否到达终点 + if (row == m_endRow && col == m_endCol) { + m_state = ALG_FOUND; + reconstructPath(); + return true; + } + + // 探索相邻节点 + static const int dx4[] = {0, 0, 1, -1}; + static const int dy4[] = {1, -1, 0, 0}; + static const int dx8[] = {0, 0, 1, -1, 1, 1, -1, -1}; + static const int dy8[] = {1, -1, 0, 0, 1, -1, 1, -1}; + + int numDirs = m_allowDiagonal ? 8 : 4; + const int* dx = m_allowDiagonal ? dx8 : dx4; + const int* dy = m_allowDiagonal ? dy8 : dy4; + + for (int i = 0; i < numDirs; ++i) { + int newRow = row + dy[i]; + int newCol = col + dx[i]; + + // 检查边界 + if (newRow < 0 || newRow >= GRID_ROWS || newCol < 0 || newCol >= GRID_COLS) { + continue; + } + + // 检查是否是墙壁或已关闭 + if (m_grid[newRow][newCol] == CELL_WALL || m_cellState[newRow][newCol] == STATE_CLOSED) { + continue; + } + + // 对角线移动时检查是否被墙壁阻挡 + if (m_allowDiagonal && i >= 4) { + int checkRow1 = row + dy[i]; + int checkCol1 = col; + int checkRow2 = row; + int checkCol2 = col + dx[i]; + + if (m_grid[checkRow1][checkCol1] == CELL_WALL && m_grid[checkRow2][checkCol2] == CELL_WALL) { + continue; + } + } + + // 计算新的 g 值 + float moveCost = (i >= 4) ? 1.414f : 1.0f; // 对角线移动代价为 √2 + float newG = m_nodeInfo[row][col].g + moveCost; + + // 检查是否已在开放列表中 + int nodeKey = newRow * GRID_COLS + newCol; + bool inOpenSet = m_openSetLookup.find(nodeKey) != m_openSetLookup.end(); + + if (!inOpenSet || newG < m_nodeInfo[newRow][newCol].g) { + // 更新节点信息 + Node& neighbor = m_nodeInfo[newRow][newCol]; + neighbor.g = newG; + neighbor.h = heuristic(newRow, newCol, m_endRow, m_endCol); + neighbor.f = neighbor.g + neighbor.h; + neighbor.parentRow = row; + neighbor.parentCol = col; + + if (!inOpenSet) { + m_openSet.push(neighbor); + m_openSetLookup.insert(nodeKey); + m_cellState[newRow][newCol] = STATE_OPEN; + } + } + } + + return false; + } + + // 启发式函数 (使用欧几里得距离) + float heuristic(int r1, int c1, int r2, int c2) + { + float dx = static_cast(c2 - c1); + float dy = static_cast(r2 - r1); + + if (m_allowDiagonal) { + // 对角线距离 (Chebyshev 距离的变体) + return std::sqrt(dx * dx + dy * dy); + } else { + // 曼哈顿距离 + return std::abs(dx) + std::abs(dy); + } + } + + // 回溯重建路径 + void reconstructPath() + { + int row = m_endRow; + int col = m_endCol; + + while (row != m_startRow || col != m_startCol) { + if (m_grid[row][col] != CELL_START && m_grid[row][col] != CELL_END) { + m_cellState[row][col] = STATE_PATH; + } + ++m_pathLength; + + int parentRow = m_nodeInfo[row][col].parentRow; + int parentCol = m_nodeInfo[row][col].parentCol; + + if (parentRow < 0 || parentCol < 0) break; + + row = parentRow; + col = parentCol; + } + } + + // 绘制所有内容 + void draw() + { + setbkcolor(ASTAR_COLOR_BG); + cleardevice(); + + ege_enable_aa(true); + + // 绘制网格 + drawGrid(); + + // 绘制控制面板 + drawControlPanel(); + } + + // 绘制网格 + void drawGrid() + { + // 绘制单元格 + for (int r = 0; r < GRID_ROWS; ++r) { + for (int c = 0; c < GRID_COLS; ++c) { + int x = c * CELL_SIZE; + int y = r * CELL_SIZE; + + color_t fillColor = ASTAR_COLOR_BG; + + // 根据单元格类型和状态确定颜色 + if (m_grid[r][c] == CELL_WALL) { + fillColor = ASTAR_COLOR_WALL; + } else if (m_grid[r][c] == CELL_START) { + fillColor = ASTAR_COLOR_START; + } else if (m_grid[r][c] == CELL_END) { + fillColor = ASTAR_COLOR_END; + } else { + // 根据搜索状态 + switch (m_cellState[r][c]) { + case STATE_PATH: + fillColor = ASTAR_COLOR_PATH; + break; + case STATE_CLOSED: + fillColor = ASTAR_COLOR_CLOSED; + break; + case STATE_OPEN: + fillColor = ASTAR_COLOR_OPEN; + break; + default: + fillColor = ASTAR_COLOR_BG; + break; + } + } + + setfillcolor(fillColor); + bar(x + 1, y + 1, x + CELL_SIZE - 1, y + CELL_SIZE - 1); + } + } + + // 绘制网格线 + setcolor(ASTAR_COLOR_GRID); + for (int r = 0; r <= GRID_ROWS; ++r) { + line(0, r * CELL_SIZE, CANVAS_WIDTH, r * CELL_SIZE); + } + for (int c = 0; c <= GRID_COLS; ++c) { + line(c * CELL_SIZE, 0, c * CELL_SIZE, CANVAS_HEIGHT); + } + + // 绘制起点和终点标记 + drawMarker(m_startRow, m_startCol, "S", ASTAR_COLOR_START); + drawMarker(m_endRow, m_endCol, "E", ASTAR_COLOR_END); + } + + // 绘制标记 + void drawMarker(int row, int col, const char* text, color_t color) + { + int x = col * CELL_SIZE + CELL_SIZE / 2; + int y = row * CELL_SIZE + CELL_SIZE / 2; + + setfont(16, 0, TEXT_FONT_NAME); + setcolor(ege::WHITE); + settextjustify(CENTER_TEXT, CENTER_TEXT); + outtextxy(x, y, text); + } + + // 绘制控制面板 + void drawControlPanel() + { + int panelX = CANVAS_WIDTH; + + // 面板背景 + setfillcolor(EGERGB(45, 45, 55)); + bar(panelX, 0, WINDOW_WIDTH, WINDOW_HEIGHT); + + // 分隔线 + setcolor(EGERGB(80, 80, 90)); + line(panelX, 0, panelX, WINDOW_HEIGHT); + + setfont(16, 0, TEXT_FONT_NAME); + setcolor(ege::WHITE); + settextjustify(LEFT_TEXT, TOP_TEXT); + + int textX = panelX + 15; + int textY = 20; + int lineHeight = 24; + + // 标题 + setfont(18, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_WINDOW_TITLE); + textY += lineHeight + 10; + + // 分隔线 + setcolor(EGERGB(80, 80, 90)); + line(panelX + 10, textY, WINDOW_WIDTH - 10, textY); + textY += 15; + + // 状态信息 + setfont(14, 0, TEXT_FONT_NAME); + setcolor(EGERGB(150, 200, 255)); + + char buf[128]; + + // 显示网格大小 + sprintf(buf, TEXT_GRID_SIZE, GRID_COLS, GRID_ROWS); + outtextxy(textX, textY, buf); + textY += lineHeight; + + // 显示动画速度 + sprintf(buf, TEXT_ANIMATION_SPEED, m_animationSpeed); + outtextxy(textX, textY, buf); + textY += lineHeight; + + // 显示已探索节点数 + sprintf(buf, TEXT_NODES_EXPLORED, m_nodesExplored); + outtextxy(textX, textY, buf); + textY += lineHeight; + + // 显示路径长度 + if (m_state == ALG_FOUND) { + sprintf(buf, TEXT_PATH_LENGTH, m_pathLength); + outtextxy(textX, textY, buf); + } + textY += lineHeight; + + // 显示对角线移动状态 + if (m_allowDiagonal) { + setcolor(EGERGB(100, 255, 100)); + outtextxy(textX, textY, TEXT_DIAGONAL_ON); + } else { + setcolor(EGERGB(255, 150, 150)); + outtextxy(textX, textY, TEXT_DIAGONAL_OFF); + } + textY += lineHeight + 5; + + // 显示状态 + switch (m_state) { + case ALG_READY: + setcolor(EGERGB(100, 255, 100)); + outtextxy(textX, textY, TEXT_STATUS_READY); + break; + case ALG_SEARCHING: + setcolor(EGERGB(255, 200, 100)); + outtextxy(textX, textY, TEXT_STATUS_SEARCHING); + break; + case ALG_FOUND: + setcolor(EGERGB(100, 255, 200)); + outtextxy(textX, textY, TEXT_STATUS_FOUND); + break; + case ALG_NO_PATH: + setcolor(EGERGB(255, 100, 100)); + outtextxy(textX, textY, TEXT_STATUS_NO_PATH); + break; + case ALG_AUTO: + setcolor(EGERGB(255, 150, 200)); + outtextxy(textX, textY, TEXT_STATUS_AUTO); + break; + } + textY += lineHeight + 10; + + // 分隔线 + setcolor(EGERGB(80, 80, 90)); + line(panelX + 10, textY, WINDOW_WIDTH - 10, textY); + textY += 15; + + // 图例 + setcolor(EGERGB(200, 200, 200)); + outtextxy(textX, textY, TEXT_LEGEND_TITLE); + textY += lineHeight; + + drawLegendItem(textX, textY, ASTAR_COLOR_START, TEXT_LEGEND_START); + textY += 20; + drawLegendItem(textX, textY, ASTAR_COLOR_END, TEXT_LEGEND_END); + textY += 20; + drawLegendItem(textX, textY, ASTAR_COLOR_WALL, TEXT_LEGEND_WALL); + textY += 20; + drawLegendItem(textX, textY, ASTAR_COLOR_OPEN, TEXT_LEGEND_OPEN); + textY += 20; + drawLegendItem(textX, textY, ASTAR_COLOR_CLOSED, TEXT_LEGEND_CLOSED); + textY += 20; + drawLegendItem(textX, textY, ASTAR_COLOR_PATH, TEXT_LEGEND_PATH); + textY += 25; + + // 分隔线 + setcolor(EGERGB(80, 80, 90)); + line(panelX + 10, textY, WINDOW_WIDTH - 10, textY); + textY += 15; + + // 控制说明 + setcolor(EGERGB(200, 200, 200)); + outtextxy(textX, textY, TEXT_CONTROLS_TITLE); + textY += lineHeight; + + setcolor(EGERGB(180, 180, 180)); + setfont(11, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_CONTROLS_START); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_RESET); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_CLEAR); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_GENERATE); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_AUTO); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_SPEED); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_DIAGONAL); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_EXIT); + textY += 25; + + // 鼠标操作说明 + setcolor(EGERGB(200, 200, 200)); + setfont(14, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_MOUSE_HINT); + textY += lineHeight; + + setcolor(EGERGB(180, 180, 180)); + setfont(11, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_MOUSE_LEFT); + textY += 18; + outtextxy(textX, textY, TEXT_MOUSE_RIGHT); + } + + // 绘制图例项 + void drawLegendItem(int x, int y, color_t color, const char* text) + { + setfillcolor(color); + bar(x, y + 2, x + 14, y + 16); + setcolor(ege::WHITE); + outtextxy(x + 20, y, text); + } + + // 处理键盘输入 + void handleInput() + { + while (kbhit()) { + int key = getch(); + + switch (key) { + case 'S': + case 's': + case ' ': + case '\r': + case '\n': + if (m_state == ALG_READY) { + initSearch(); + } + if (m_state == ALG_SEARCHING) { + stepSearch(); + } + break; + + case 'R': + case 'r': + resetSearch(); + break; + + case 'C': + case 'c': + clearWalls(); + break; + + case 'G': + case 'g': + generateMaze(); + break; + + case 'A': + case 'a': + m_autoMode = !m_autoMode; + if (m_autoMode) { + if (m_state == ALG_READY) { + initSearch(); + } + m_state = ALG_AUTO; + } else { + if (m_state == ALG_AUTO) { + m_state = ALG_SEARCHING; + } + } + break; + + case 'D': + case 'd': + m_allowDiagonal = !m_allowDiagonal; + resetSearch(); + break; + + case key_up: + m_animationSpeed = std::max(5, m_animationSpeed - 10); + break; + + case key_down: + m_animationSpeed = std::min(500, m_animationSpeed + 10); + break; + + case key_esc: + closegraph(); + exit(0); + break; + } + } + } + + // 处理鼠标输入 + void handleMouse() + { + static bool leftButtonDown = false; + static bool rightButtonDown = false; + static int lastRow = -1; + static int lastCol = -1; + + while (mousemsg()) { + mouse_msg msg = getmouse(); + + int col = msg.x / CELL_SIZE; + int row = msg.y / CELL_SIZE; + + // 确保在网格范围内 + if (col < 0 || col >= GRID_COLS || row < 0 || row >= GRID_ROWS) { + continue; + } + + if (msg.is_left()) { + if (msg.is_down()) { + leftButtonDown = true; + lastRow = row; + lastCol = col; + + // 切换墙壁状态 + if (m_grid[row][col] == CELL_EMPTY) { + m_grid[row][col] = CELL_WALL; + resetSearch(); + } else if (m_grid[row][col] == CELL_WALL) { + m_grid[row][col] = CELL_EMPTY; + resetSearch(); + } + } else if (msg.is_up()) { + leftButtonDown = false; + } else if (msg.is_move() && leftButtonDown) { + // 拖动绘制墙壁 + if (row != lastRow || col != lastCol) { + if (m_grid[row][col] == CELL_EMPTY) { + m_grid[row][col] = CELL_WALL; + resetSearch(); + } + lastRow = row; + lastCol = col; + } + } + } + + if (msg.is_right()) { + if (msg.is_down()) { + rightButtonDown = true; + + // 设置起点或终点 + if (m_grid[row][col] != CELL_WALL) { + if (m_settingStart) { + // 清除旧起点 + m_grid[m_startRow][m_startCol] = CELL_EMPTY; + // 设置新起点 + m_startRow = row; + m_startCol = col; + m_grid[row][col] = CELL_START; + } else { + // 清除旧终点 + m_grid[m_endRow][m_endCol] = CELL_EMPTY; + // 设置新终点 + m_endRow = row; + m_endCol = col; + m_grid[row][col] = CELL_END; + } + m_settingStart = !m_settingStart; + resetSearch(); + } + } else if (msg.is_up()) { + rightButtonDown = false; + } + } + } + } + + // 自动演示模式更新 + void autoUpdate() + { + if (m_autoMode && (m_state == ALG_SEARCHING || m_state == ALG_AUTO)) { + m_state = ALG_AUTO; + if (stepSearch()) { + m_autoMode = false; + } + } + } + + // 获取动画速度 + int getAnimationSpeed() const { return m_animationSpeed; } + + // 是否在自动模式 + bool isAutoMode() const { return m_autoMode; } + + // 启动自动演示 (生成迷宫并开始自动搜索) + void startAutoDemo() + { + generateMaze(); + initSearch(); + m_autoMode = true; + m_state = ALG_AUTO; + } + +private: + std::vector> m_grid; // 网格类型 + std::vector> m_cellState; // 单元格状态 + std::vector> m_nodeInfo; // 节点信息 + + // A* 算法数据结构 + std::priority_queue, std::greater> m_openSet; + std::set m_openSetLookup; // 快速查找开放集合 + + int m_startRow, m_startCol; // 起点 + int m_endRow, m_endCol; // 终点 + + AlgorithmState m_state; // 算法状态 + int m_animationSpeed; + bool m_autoMode; + bool m_allowDiagonal; + int m_pathLength; + int m_nodesExplored; + bool m_settingStart; // 用于交替设置起点和终点 +}; + +/** + * @brief 程序入口 + */ +int main() +{ + // 初始化图形窗口 + setinitmode(INIT_ANIMATION); + initgraph(WINDOW_WIDTH, WINDOW_HEIGHT); + setcaption(TEXT_WINDOW_TITLE); + setbkmode(TRANSPARENT); + + // 创建可视化器 + AStarVisualizer visualizer; + + // 启动时自动生成迷宫并开始演示 + visualizer.startAutoDemo(); + + // 主循环 + int frameCount = 0; + for (; is_run(); delay_fps(60)) { + // 处理输入 + visualizer.handleInput(); + visualizer.handleMouse(); + + // 自动模式更新 + if (visualizer.isAutoMode()) { + int framesPerStep = visualizer.getAnimationSpeed() / 16; + if (framesPerStep < 1) framesPerStep = 1; + if (++frameCount >= framesPerStep) { + visualizer.autoUpdate(); + frameCount = 0; + } + } + + // 绘制 + visualizer.draw(); + } + + closegraph(); + return 0; +} diff --git a/cmake_template/ege_demos/graph_ball.cpp b/cmake_template/ege_demos/graph_ball.cpp new file mode 100644 index 0000000..a97eb81 --- /dev/null +++ b/cmake_template/ege_demos/graph_ball.cpp @@ -0,0 +1,171 @@ +/******************************************************************** + * @file egeball.cpp + * @brief This program demonstrates a physics collision simulation of bouncing balls. + ********************************************************************/ + +#include +#include +#include + +#define myrand(m) ((float)random(10000) * m / 10000.0f) +#define IsCrash(a, b) ((a.x - b.x)*(a.x - b.x)+(a.y - b.y)*(a.y - b.y) < (a.r + b.r)*(a.r + b.r)) +#define IsWEdge(a) (a.x < a.r || a.x >= 640-a.r) +#define IsHEdge(a) (a.y < a.r || a.y >= 480-a.r) +#define IsEdge(a) (IsWEdge(a) || IsHEdge(a)) +#define Distance(x1, y1, x2, y2) (((x1) - (x2))*((x1) - (x2)) + ((y1) - (y2))*((y1) - (y2))) + +/** + * @struct Obj + * @brief Represents a ball object with its position, velocity, radius, and color. + */ +typedef struct { + float x, y; + float vx, vy; + int r; + int color; +} Obj; + +/** + * @class AniObj + * @brief Represents a collection of animated ball objects with collision simulation. + */ +class AniObj { +public: + /** + * @brief Constructor for AniObj. + * Initializes the AniObj object with random ball positions, sizes, and colors. + */ + AniObj() { + n = 8; + int i, j; + bool goon; + for ( i = 0; i < n; i++ ) { + do { + goon = false; + obj[i].x = ( float )random( getwidth() ); + obj[i].y = ( float )random( getheight() ); + obj[i].r = random( 40 ) + 20; + if ( IsEdge( obj[i] ) ) + goon = true; + else if ( i != 0 ) + for ( j = i - 1; j >= 0; j-- ) + if ( IsCrash( obj[i], obj[j] ) ) { + goon = true; + break; + } + } while ( goon ); + obj[i].vx = obj[i].vy = 0.0f; + obj[i].color = HSVtoRGB( myrand( 360.0f ), 1.0f, 1.0f ); + } + } + + /** + * @brief Updates the positions and velocities of the ball objects based on physics laws. + */ + void updateobj() { + int i, j; + for ( i = 0; i < n; i++ ) { + obj[i].vy += 0.05f; + obj[i].x += obj[i].vx; + obj[i].y += obj[i].vy; + if ( obj[i].y >= 480 - obj[i].r && obj[i].vy > 0 ) { + obj[i].y -= obj[i].vy; + obj[i].vy = - obj[i].vy; + } + if ( obj[i].x < obj[i].r && obj[i].vx < 0 ) obj[i].vx = - obj[i].vx; + if ( obj[i].x >= 640 - obj[i].r && obj[i].vx > 0 ) obj[i].vx = - obj[i].vx; + } + for ( i = 1; i < n; i++ ) + for ( j = i - 1; j >= 0; j-- ) + if ( IsCrash( obj[i], obj[j] ) && + ( Distance( obj[i].x, obj[i].y, obj[j].x, obj[j].y ) > + Distance( obj[i].x + obj[i].vx, obj[i].y + obj[i].vy, obj[j].x + obj[j].vx, obj[j].y + obj[j].vy ) + ) + ) + Crash( obj[i], obj[j] ); + } + + /** + * @brief Draws the ball objects on the graphics window. + */ + void drawobj() { + for ( int i = 0; i < n; i++ ) { + setfillcolor( obj[i].color ); + ege_fillellipse( obj[i].x - obj[i].r, obj[i].y - obj[i].r, + obj[i].r * 2, obj[i].r * 2 ); + } + } + + /** + * @brief Destructor for AniObj. + * Cleans up any resources associated with the AniObj object. + */ + ~AniObj() { + } + +private: + /** + * @brief Updates the velocities of two colliding ball objects using the physics of elastic collision. + */ + void Crash( Obj &a, Obj &b ) { + float ma = a.r * a.r, mb = b.r * b.r; + + float sx = a.x - b.x; + float sy = a.y - b.y; + float s1x = sx / sqrt( sx*sx + sy*sy ); + float s1y = sy / sqrt( sx*sx + sy*sy ); + float t1x = -s1y; + float t1y = s1x; + + float vas = a.vx * s1x + a.vy * s1y; + float vat = a.vx * t1x + a.vy * t1y; + float vbs = b.vx * s1x + b.vy * s1y; + float vbt = b.vx * t1x + b.vy * t1y; + + float vasf = ( 2 * mb * vbs + vas * ( ma - mb ) ) / ( ma + mb ); + float vbsf = ( 2 * ma * vas - vbs * ( ma - mb ) ) / ( ma + mb ); + + float nsx = vasf * s1x; + float nsy = vasf * s1y; + float ntx = vat * t1x; + float nty = vat * t1y; + + a.vx = nsx + ntx; + a.vy = nsy + nty; + + nsx = vbsf * s1x; + nsy = vbsf * s1y; + ntx = vbt * t1x; + nty = vbt * t1y; + + b.vx = nsx + ntx; + b.vy = nsy + nty; + } + +private: + Obj obj[20]; + int n; +}; + +/** + * @brief Entry point of the program. + * Initializes the graphics window and runs the animation loop. + */ +int main() { + setinitmode( INIT_ANIMATION ); + initgraph( 640, 480 ); + randomize(); // Initialize random seed + + AniObj aniobj; // Create AniObj object + fps f; + ege_enable_aa( true ); + + for (; is_run(); delay_fps(120)) { + aniobj.updateobj(); // Update object positions + cleardevice(); + aniobj.drawobj(); // Draw objects + } + + closegraph(); + return 0; +} diff --git a/cmake_template/ege_demos/graph_boids.cpp b/cmake_template/ege_demos/graph_boids.cpp new file mode 100644 index 0000000..33d0ac0 --- /dev/null +++ b/cmake_template/ege_demos/graph_boids.cpp @@ -0,0 +1,784 @@ +/** + * @file graph_boids.cpp + * @author wysaid (this@xege.org) + * @brief Boids 群集模拟可视化演示 + * @version 0.1 + * @date 2025-11-27 + * + * Boids 是 Craig Reynolds 在 1986 年提出的模拟群体行为的算法。 + * 通过三条简单规则产生复杂的涌现行为: + * 1. 分离 (Separation) - 避免与邻近个体碰撞 + * 2. 对齐 (Alignment) - 与邻近个体保持相同方向 + * 3. 凝聚 (Cohesion) - 向邻近个体的中心靠拢 + * + * 本程序通过动画演示 Boids 算法的群集行为,可用于模拟鸟群、鱼群等。 + */ + +#ifndef NOMINMAX +#define NOMINMAX 1 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +// 文本本地化宏定义 +#ifdef _MSC_VER +// MSVC编译器使用中文文案 +#define TEXT_WINDOW_TITLE "Boids 群集模拟" +#define TEXT_CONTROLS_TITLE "按键说明:" +#define TEXT_CONTROLS_ADD "+/= - 增加 Boid 数量" +#define TEXT_CONTROLS_SUB "-/_ - 减少 Boid 数量" +#define TEXT_CONTROLS_RESET "R - 重置所有 Boids" +#define TEXT_CONTROLS_PREDATOR "P - 切换捕食者模式" +#define TEXT_CONTROLS_TRAIL "T - 切换轨迹显示" +#define TEXT_CONTROLS_SPEED "↑/↓ - 调整速度" +#define TEXT_CONTROLS_EXIT "ESC - 退出程序" +#define TEXT_MOUSE_HINT "鼠标操作:" +#define TEXT_MOUSE_LEFT "左键 - 吸引 Boids" +#define TEXT_MOUSE_RIGHT "右键 - 驱散 Boids" +#define TEXT_BOID_COUNT "Boid 数量: %d" +#define TEXT_SPEED_MULT "速度倍率: %.1fx" +#define TEXT_PREDATOR_ON "捕食者模式: 开启" +#define TEXT_PREDATOR_OFF "捕食者模式: 关闭" +#define TEXT_TRAIL_ON "轨迹显示: 开启" +#define TEXT_TRAIL_OFF "轨迹显示: 关闭" +#define TEXT_RULES_TITLE "规则权重:" +#define TEXT_RULE_SEPARATION "分离: %.2f (1/2调整)" +#define TEXT_RULE_ALIGNMENT "对齐: %.2f (3/4调整)" +#define TEXT_RULE_COHESION "凝聚: %.2f (5/6调整)" +#define TEXT_FONT_NAME "宋体" +#else +// 非MSVC编译器使用英文文案 +#define TEXT_WINDOW_TITLE "Boids Flocking Simulation" +#define TEXT_CONTROLS_TITLE "Controls:" +#define TEXT_CONTROLS_ADD "+/= - Add Boids" +#define TEXT_CONTROLS_SUB "-/_ - Remove Boids" +#define TEXT_CONTROLS_RESET "R - Reset All Boids" +#define TEXT_CONTROLS_PREDATOR "P - Toggle Predator Mode" +#define TEXT_CONTROLS_TRAIL "T - Toggle Trail" +#define TEXT_CONTROLS_SPEED "Up/Down - Adjust Speed" +#define TEXT_CONTROLS_EXIT "ESC - Exit" +#define TEXT_MOUSE_HINT "Mouse:" +#define TEXT_MOUSE_LEFT "Left - Attract Boids" +#define TEXT_MOUSE_RIGHT "Right - Repel Boids" +#define TEXT_BOID_COUNT "Boid Count: %d" +#define TEXT_SPEED_MULT "Speed: %.1fx" +#define TEXT_PREDATOR_ON "Predator Mode: ON" +#define TEXT_PREDATOR_OFF "Predator Mode: OFF" +#define TEXT_TRAIL_ON "Trail: ON" +#define TEXT_TRAIL_OFF "Trail: OFF" +#define TEXT_RULES_TITLE "Rule Weights:" +#define TEXT_RULE_SEPARATION "Separation: %.2f (1/2)" +#define TEXT_RULE_ALIGNMENT "Alignment: %.2f (3/4)" +#define TEXT_RULE_COHESION "Cohesion: %.2f (5/6)" +#define TEXT_FONT_NAME "Arial" +#endif + +// 窗口参数 +const int WINDOW_WIDTH = 1200; +const int WINDOW_HEIGHT = 800; +const int PANEL_WIDTH = 220; +const int CANVAS_WIDTH = WINDOW_WIDTH - PANEL_WIDTH; +const int CANVAS_HEIGHT = WINDOW_HEIGHT; + +// Boid 参数 +const int DEFAULT_BOID_COUNT = 150; +const int MIN_BOID_COUNT = 10; +const int MAX_BOID_COUNT = 500; +const float BOID_SIZE = 8.0f; +const float MAX_SPEED = 4.0f; +const float MAX_FORCE = 0.15f; +const float PERCEPTION_RADIUS = 50.0f; +const float SEPARATION_RADIUS = 25.0f; + +// 颜色定义 +const ege::color_t BOIDS_COLOR_BG = EGERGB(20, 25, 35); +const ege::color_t BOIDS_COLOR_PANEL = EGERGB(35, 40, 50); +const ege::color_t BOIDS_COLOR_BOID = EGERGB(100, 200, 255); +const ege::color_t BOIDS_COLOR_PRED = EGERGB(255, 80, 80); +const ege::color_t BOIDS_COLOR_TRAIL = EGERGB(60, 100, 140); + +/** + * @struct Vec2 + * @brief 2D 向量结构 + */ +struct Vec2 +{ + float x, y; + + Vec2(float _x = 0, float _y = 0) : x(_x), y(_y) {} + + Vec2 operator+(const Vec2& v) const { return Vec2(x + v.x, y + v.y); } + Vec2 operator-(const Vec2& v) const { return Vec2(x - v.x, y - v.y); } + Vec2 operator*(float s) const { return Vec2(x * s, y * s); } + Vec2 operator/(float s) const { return Vec2(x / s, y / s); } + + Vec2& operator+=(const Vec2& v) + { + x += v.x; + y += v.y; + return *this; + } + Vec2& operator-=(const Vec2& v) + { + x -= v.x; + y -= v.y; + return *this; + } + Vec2& operator*=(float s) + { + x *= s; + y *= s; + return *this; + } + + float magnitude() const { return std::sqrt(x * x + y * y); } + + float magnitudeSq() const { return x * x + y * y; } + + Vec2 normalized() const + { + float m = magnitude(); + if (m > 0) return Vec2(x / m, y / m); + return Vec2(0, 0); + } + + void limit(float max) + { + float m = magnitude(); + if (m > max) { + x = x / m * max; + y = y / m * max; + } + } + + void setMagnitude(float mag) + { + float m = magnitude(); + if (m > 0) { + x = x / m * mag; + y = y / m * mag; + } + } + + static float distance(const Vec2& a, const Vec2& b) + { + float dx = a.x - b.x; + float dy = a.y - b.y; + return std::sqrt(dx * dx + dy * dy); + } + + static float distanceSq(const Vec2& a, const Vec2& b) + { + float dx = a.x - b.x; + float dy = a.y - b.y; + return dx * dx + dy * dy; + } +}; + +/** + * @struct Boid + * @brief Boid 个体结构 + */ +struct Boid +{ + Vec2 position; + Vec2 velocity; + Vec2 acceleration; + float hue; // 颜色色相 (用于个体差异) + std::vector trail; // 轨迹历史 + + Boid(float x, float y) + { + position = Vec2(x, y); + // 随机初始速度 + float angle = static_cast(rand()) / RAND_MAX * 2.0f * 3.14159f; + float speed = static_cast(rand()) / RAND_MAX * 2.0f + 1.0f; + velocity = Vec2(std::cos(angle) * speed, std::sin(angle) * speed); + acceleration = Vec2(0, 0); + hue = static_cast(rand()) / RAND_MAX * 60.0f + 180.0f; // 180-240 蓝绿色调 + } + + // 应用力 + void applyForce(const Vec2& force) { acceleration += force; } + + // 更新位置和速度 + void update(float speedMult) + { + velocity += acceleration * speedMult; + velocity.limit(MAX_SPEED * speedMult); + position += velocity; + acceleration = Vec2(0, 0); + + // 更新轨迹 + trail.push_back(position); + if (trail.size() > 20) { + trail.erase(trail.begin()); + } + } + + // 边界环绕 + void wrapEdges(int width, int height) + { + if (position.x < 0) position.x = static_cast(width); + if (position.x > width) position.x = 0; + if (position.y < 0) position.y = static_cast(height); + if (position.y > height) position.y = 0; + } +}; + +/** + * @class BoidsSimulation + * @brief Boids 群集模拟类 + */ +class BoidsSimulation +{ +public: + BoidsSimulation() + : m_separationWeight(1.5f) + , m_alignmentWeight(1.0f) + , m_cohesionWeight(1.0f) + , m_speedMultiplier(1.0f) + , m_predatorMode(false) + , m_showTrail(false) + , m_mouseAttract(false) + , m_mouseRepel(false) + , m_mouseX(0) + , m_mouseY(0) + { + srand(static_cast(time(nullptr))); + initBoids(DEFAULT_BOID_COUNT); + } + + // 初始化 Boids + void initBoids(int count) + { + m_boids.clear(); + for (int i = 0; i < count; ++i) { + float x = static_cast(rand() % CANVAS_WIDTH); + float y = static_cast(rand() % CANVAS_HEIGHT); + m_boids.push_back(Boid(x, y)); + } + } + + // 添加 Boids + void addBoids(int count) + { + for (int i = 0; i < count && static_cast(m_boids.size()) < MAX_BOID_COUNT; ++i) { + float x = static_cast(rand() % CANVAS_WIDTH); + float y = static_cast(rand() % CANVAS_HEIGHT); + m_boids.push_back(Boid(x, y)); + } + } + + // 移除 Boids + void removeBoids(int count) + { + for (int i = 0; i < count && static_cast(m_boids.size()) > MIN_BOID_COUNT; ++i) { + m_boids.pop_back(); + } + } + + // 分离行为 - 避免拥挤 + Vec2 separation(const Boid& boid) + { + Vec2 steering(0, 0); + int count = 0; + float radiusSq = SEPARATION_RADIUS * SEPARATION_RADIUS; + + for (const auto& other : m_boids) { + float d = Vec2::distanceSq(boid.position, other.position); + if (d > 0 && d < radiusSq) { + Vec2 diff = boid.position - other.position; + diff = diff.normalized(); + diff = diff / std::sqrt(d); // 距离越近,斥力越大 + steering += diff; + ++count; + } + } + + if (count > 0) { + steering = steering / static_cast(count); + if (steering.magnitude() > 0) { + steering.setMagnitude(MAX_SPEED); + steering = steering - boid.velocity; + steering.limit(MAX_FORCE); + } + } + + return steering; + } + + // 对齐行为 - 与邻居保持相同方向 + Vec2 alignment(const Boid& boid) + { + Vec2 steering(0, 0); + int count = 0; + float radiusSq = PERCEPTION_RADIUS * PERCEPTION_RADIUS; + + for (const auto& other : m_boids) { + float d = Vec2::distanceSq(boid.position, other.position); + if (d > 0 && d < radiusSq) { + steering += other.velocity; + ++count; + } + } + + if (count > 0) { + steering = steering / static_cast(count); + steering.setMagnitude(MAX_SPEED); + steering = steering - boid.velocity; + steering.limit(MAX_FORCE); + } + + return steering; + } + + // 凝聚行为 - 向邻居中心靠拢 + Vec2 cohesion(const Boid& boid) + { + Vec2 steering(0, 0); + int count = 0; + float radiusSq = PERCEPTION_RADIUS * PERCEPTION_RADIUS; + + for (const auto& other : m_boids) { + float d = Vec2::distanceSq(boid.position, other.position); + if (d > 0 && d < radiusSq) { + steering += other.position; + ++count; + } + } + + if (count > 0) { + steering = steering / static_cast(count); + steering = steering - boid.position; + steering.setMagnitude(MAX_SPEED); + steering = steering - boid.velocity; + steering.limit(MAX_FORCE); + } + + return steering; + } + + // 鼠标交互力 + Vec2 mouseForce(const Boid& boid) + { + Vec2 steering(0, 0); + + if (!m_mouseAttract && !m_mouseRepel) return steering; + + Vec2 mousePos(static_cast(m_mouseX), static_cast(m_mouseY)); + float d = Vec2::distance(boid.position, mousePos); + + if (d < 150.0f) { + Vec2 diff = mousePos - boid.position; + diff = diff.normalized(); + + if (m_mouseRepel) { + diff = diff * -1.0f; // 反向 + } + + float strength = (150.0f - d) / 150.0f; // 距离越近,力越大 + diff = diff * (MAX_FORCE * 3.0f * strength); + steering = diff; + } + + return steering; + } + + // 更新所有 Boids + void update() + { + for (auto& boid : m_boids) { + Vec2 sep = separation(boid) * m_separationWeight; + Vec2 ali = alignment(boid) * m_alignmentWeight; + Vec2 coh = cohesion(boid) * m_cohesionWeight; + Vec2 mouse = mouseForce(boid); + + boid.applyForce(sep); + boid.applyForce(ali); + boid.applyForce(coh); + boid.applyForce(mouse); + + boid.update(m_speedMultiplier); + boid.wrapEdges(CANVAS_WIDTH, CANVAS_HEIGHT); + } + } + + // 绘制所有内容 + void draw() + { + setbkcolor(BOIDS_COLOR_BG); + cleardevice(); + + ege_enable_aa(true); + + // 绘制轨迹 + if (m_showTrail) { + drawTrails(); + } + + // 绘制 Boids + drawBoids(); + + // 绘制鼠标影响范围 + if (m_mouseAttract || m_mouseRepel) { + drawMouseInfluence(); + } + + // 绘制控制面板 + drawControlPanel(); + } + + // 绘制轨迹 + void drawTrails() + { + for (const auto& boid : m_boids) { + if (boid.trail.size() < 2) continue; + + for (size_t i = 1; i < boid.trail.size(); ++i) { + float alpha = static_cast(i) / static_cast(boid.trail.size()); + ege::color_t color = EGEACOLOR(static_cast(alpha * 100), BOIDS_COLOR_TRAIL); + setcolor(color); + setlinestyle(PS_SOLID, 1); + line(static_cast(boid.trail[i - 1].x), static_cast(boid.trail[i - 1].y), + static_cast(boid.trail[i].x), static_cast(boid.trail[i].y)); + } + } + } + + // 绘制 Boids + void drawBoids() + { + for (const auto& boid : m_boids) { + // 计算朝向角度 + float angle = std::atan2(boid.velocity.y, boid.velocity.x); + + // 三角形三个顶点 + float size = BOID_SIZE; + float x1 = boid.position.x + std::cos(angle) * size * 1.5f; + float y1 = boid.position.y + std::sin(angle) * size * 1.5f; + float x2 = boid.position.x + std::cos(angle + 2.5f) * size; + float y2 = boid.position.y + std::sin(angle + 2.5f) * size; + float x3 = boid.position.x + std::cos(angle - 2.5f) * size; + float y3 = boid.position.y + std::sin(angle - 2.5f) * size; + + // 根据色相生成颜色 + ege::color_t color = HSVtoRGB(boid.hue, 0.8f, 1.0f); + if (m_predatorMode) { + color = BOIDS_COLOR_PRED; + } + + setfillcolor(color); + setcolor(EGEACOLOR(200, color)); + + // 绘制三角形 + ege_point points[3]; + points[0].x = x1; + points[0].y = y1; + points[1].x = x2; + points[1].y = y2; + points[2].x = x3; + points[2].y = y3; + + ege_fillpoly(3, points); + } + } + + // 绘制鼠标影响范围 + void drawMouseInfluence() + { + ege::color_t color = m_mouseAttract ? EGERGB(100, 200, 100) : EGERGB(200, 100, 100); + setcolor(EGEACOLOR(100, color)); + setlinestyle(PS_DOT, 2); + circle(m_mouseX, m_mouseY, 150); + setlinestyle(PS_SOLID, 1); + } + + // 绘制控制面板 + void drawControlPanel() + { + int panelX = CANVAS_WIDTH; + + // 面板背景 + setfillcolor(BOIDS_COLOR_PANEL); + bar(panelX, 0, WINDOW_WIDTH, WINDOW_HEIGHT); + + // 分隔线 + setcolor(EGERGB(60, 65, 75)); + line(panelX, 0, panelX, WINDOW_HEIGHT); + + setfont(16, 0, TEXT_FONT_NAME); + setcolor(ege::WHITE); + settextjustify(LEFT_TEXT, TOP_TEXT); + + int textX = panelX + 15; + int textY = 20; + int lineHeight = 24; + + // 标题 + setfont(18, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_WINDOW_TITLE); + textY += lineHeight + 10; + + // 分隔线 + setcolor(EGERGB(60, 65, 75)); + line(panelX + 10, textY, WINDOW_WIDTH - 10, textY); + textY += 15; + + // 状态信息 + setfont(14, 0, TEXT_FONT_NAME); + setcolor(EGERGB(150, 200, 255)); + + char buf[128]; + + // Boid 数量 + sprintf(buf, TEXT_BOID_COUNT, static_cast(m_boids.size())); + outtextxy(textX, textY, buf); + textY += lineHeight; + + // 速度倍率 + sprintf(buf, TEXT_SPEED_MULT, m_speedMultiplier); + outtextxy(textX, textY, buf); + textY += lineHeight; + + // 捕食者模式 + if (m_predatorMode) { + setcolor(EGERGB(255, 150, 150)); + outtextxy(textX, textY, TEXT_PREDATOR_ON); + } else { + setcolor(EGERGB(150, 150, 150)); + outtextxy(textX, textY, TEXT_PREDATOR_OFF); + } + textY += lineHeight; + + // 轨迹显示 + if (m_showTrail) { + setcolor(EGERGB(150, 255, 150)); + outtextxy(textX, textY, TEXT_TRAIL_ON); + } else { + setcolor(EGERGB(150, 150, 150)); + outtextxy(textX, textY, TEXT_TRAIL_OFF); + } + textY += lineHeight + 10; + + // 分隔线 + setcolor(EGERGB(60, 65, 75)); + line(panelX + 10, textY, WINDOW_WIDTH - 10, textY); + textY += 15; + + // 规则权重 + setcolor(EGERGB(200, 200, 200)); + outtextxy(textX, textY, TEXT_RULES_TITLE); + textY += lineHeight; + + setcolor(EGERGB(255, 200, 150)); + sprintf(buf, TEXT_RULE_SEPARATION, m_separationWeight); + outtextxy(textX, textY, buf); + textY += 20; + + setcolor(EGERGB(150, 255, 200)); + sprintf(buf, TEXT_RULE_ALIGNMENT, m_alignmentWeight); + outtextxy(textX, textY, buf); + textY += 20; + + setcolor(EGERGB(150, 200, 255)); + sprintf(buf, TEXT_RULE_COHESION, m_cohesionWeight); + outtextxy(textX, textY, buf); + textY += 25; + + // 分隔线 + setcolor(EGERGB(60, 65, 75)); + line(panelX + 10, textY, WINDOW_WIDTH - 10, textY); + textY += 15; + + // 控制说明 + setcolor(EGERGB(200, 200, 200)); + outtextxy(textX, textY, TEXT_CONTROLS_TITLE); + textY += lineHeight; + + setcolor(EGERGB(180, 180, 180)); + setfont(11, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_CONTROLS_ADD); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_SUB); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_RESET); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_PREDATOR); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_TRAIL); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_SPEED); + textY += 18; + outtextxy(textX, textY, TEXT_CONTROLS_EXIT); + textY += 25; + + // 鼠标操作说明 + setcolor(EGERGB(200, 200, 200)); + setfont(14, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_MOUSE_HINT); + textY += lineHeight; + + setcolor(EGERGB(180, 180, 180)); + setfont(11, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_MOUSE_LEFT); + textY += 18; + outtextxy(textX, textY, TEXT_MOUSE_RIGHT); + } + + // 处理键盘输入 + void handleInput() + { + while (kbhit()) { + int key = getch(); + + switch (key) { + case '+': + case '=': + addBoids(10); + break; + + case '-': + case '_': + removeBoids(10); + break; + + case 'R': + case 'r': + initBoids(DEFAULT_BOID_COUNT); + break; + + case 'P': + case 'p': + m_predatorMode = !m_predatorMode; + if (m_predatorMode) { + m_separationWeight = 3.0f; + m_alignmentWeight = 0.5f; + m_cohesionWeight = 0.3f; + } else { + m_separationWeight = 1.5f; + m_alignmentWeight = 1.0f; + m_cohesionWeight = 1.0f; + } + break; + + case 'T': + case 't': + m_showTrail = !m_showTrail; + // 清除轨迹 + if (!m_showTrail) { + for (auto& boid : m_boids) { + boid.trail.clear(); + } + } + break; + + case key_up: + m_speedMultiplier = std::min(3.0f, m_speedMultiplier + 0.1f); + break; + + case key_down: + m_speedMultiplier = std::max(0.3f, m_speedMultiplier - 0.1f); + break; + + // 规则权重调整 + case '1': + m_separationWeight = std::max(0.0f, m_separationWeight - 0.1f); + break; + case '2': + m_separationWeight = std::min(5.0f, m_separationWeight + 0.1f); + break; + case '3': + m_alignmentWeight = std::max(0.0f, m_alignmentWeight - 0.1f); + break; + case '4': + m_alignmentWeight = std::min(5.0f, m_alignmentWeight + 0.1f); + break; + case '5': + m_cohesionWeight = std::max(0.0f, m_cohesionWeight - 0.1f); + break; + case '6': + m_cohesionWeight = std::min(5.0f, m_cohesionWeight + 0.1f); + break; + + case key_esc: + closegraph(); + exit(0); + break; + } + } + } + + // 处理鼠标输入 + void handleMouse() + { + while (mousemsg()) { + mouse_msg msg = getmouse(); + + m_mouseX = msg.x; + m_mouseY = msg.y; + + if (msg.is_left()) { + m_mouseAttract = msg.is_down() || (msg.is_move() && m_mouseAttract); + if (msg.is_up()) m_mouseAttract = false; + } + + if (msg.is_right()) { + m_mouseRepel = msg.is_down() || (msg.is_move() && m_mouseRepel); + if (msg.is_up()) m_mouseRepel = false; + } + } + } + +private: + std::vector m_boids; + + float m_separationWeight; + float m_alignmentWeight; + float m_cohesionWeight; + float m_speedMultiplier; + + bool m_predatorMode; + bool m_showTrail; + bool m_mouseAttract; + bool m_mouseRepel; + int m_mouseX, m_mouseY; +}; + +/** + * @brief 程序入口 + */ +int main() +{ + // 初始化图形窗口 + setinitmode(INIT_ANIMATION); + initgraph(WINDOW_WIDTH, WINDOW_HEIGHT); + setcaption(TEXT_WINDOW_TITLE); + setbkmode(TRANSPARENT); + + // 创建模拟器 + BoidsSimulation simulation; + + // 主循环 + for (; is_run(); delay_fps(60)) { + // 处理输入 + simulation.handleInput(); + simulation.handleMouse(); + + // 更新 + simulation.update(); + + // 绘制 + simulation.draw(); + } + + closegraph(); + return 0; +} diff --git a/cmake_template/ege_demos/graph_catharine.cpp b/cmake_template/ege_demos/graph_catharine.cpp new file mode 100644 index 0000000..15e4384 --- /dev/null +++ b/cmake_template/ege_demos/graph_catharine.cpp @@ -0,0 +1,107 @@ +// 烟花特效演示 + +#include +#include +#include + +#define myrand(m) ((float)(randomf() * m )) + +typedef struct +{ + float x, y; + float vx, vy; + int color; +}Point; + +class AniObj +{ +public: + //初始化,设置坐标 + AniObj() + { + Init(); + } + + void Init() + { + n = 100; + float x = myrand(600.0f) + 20.0f; + float y = myrand(100.0f) + 100.0f; + for (int i = 0; i < n; i++) + { + p[i].x = x; + p[i].y = y; + p[i].vx = 1.0f - myrand(2.0f); + p[i].vy = 1.0f - myrand(2.0f); + p[i].color = HSVtoRGB(myrand(360.0f), 1.0f, 1.0f); + } + color = HSVtoRGB(myrand(360.0f), 1.0f, 1.0f); + start = rand() % 300; + cnt = 0; + } + + //更新位置等相关属性 + void updateobj() + { + if (cnt++ > start) + for (int i = 0; i < n; i++) + { + p[i].vy += 0.01f; + p[i].x += p[i].vx; + p[i].y += p[i].vy; + } + if (cnt > start + 400) Init(); + } + + //根据属性值绘画 + void drawobj() + { + for (int i = 0; i < n; i++) + { + putpixel(p[i].x, p[i].y, color); + } + } + + //释放这个对象时调用 + ~AniObj() + { + } + +private: + Point p[100]; + int n; + int color; + int start; + int cnt; +}; + + +#define MAXOBJ 20 + +int main() +{ + initgraph(640, 480); + randomize(); //初始化随机种子 + + AniObj obj[MAXOBJ]; //定义对象数组 + int n; + + setrendermode(RENDER_MANUAL); + for ( ; kbhit() == 0; delay_fps(60) ) + { + for (n = 0; n < MAXOBJ; ++n) + { + obj[n].updateobj(); //更新位置 + } + + imagefilter_blurring(NULL, 0x4F, 0x100); + for (n = 0; n < MAXOBJ; ++n) + { + obj[n].drawobj(); //绘画 + } + } + + closegraph(); + return 0; +} + diff --git a/cmake_template/ege_demos/graph_clock.cpp b/cmake_template/ege_demos/graph_clock.cpp new file mode 100644 index 0000000..42fbe1a --- /dev/null +++ b/cmake_template/ege_demos/graph_clock.cpp @@ -0,0 +1,122 @@ +#include +#include +#include +#include + +#define for if (0); else for + +/** + * @brief 根据圆心、半径和角度计算直角坐标系坐标 + * + * @param center 圆心坐标 + * @param rad 角度(弧度) + * @param r 半径 + * @return 计算得到的直角坐标系坐标 + */ +ege::ege_point getpos(ege::ege_point center, float rad, float r) +{ + ege::ege_point pos; + pos.x = sin(rad) * r + center.x; + pos.y = -cos(rad) * r + center.y; + return pos; +} + +/** + * @brief 绘制时钟 + * + * 绘制指针式时钟的表盘、数字、时针、分针、秒针以及当前时间。 + */ +void draw() +{ + float pi2 = ege::PI * 2; + ege::ege_point center; + center.x = 200, center.y = 200; + float r = 150; + ege::settextjustify(ege::CENTER_TEXT, ege::CENTER_TEXT); + ege::setfont(24, 0, "Courier New"); + ege::setbkmode(TRANSPARENT); + + ege::ege_enable_aa(true); + ege::setfillcolor(EGEARGB(0xff, 0x40, 0x40, 0x40)); + ege::setcolor(EGEARGB(0xff, 0x80, 0x00, 0xf0)); + ege::ege_fillellipse(center.x - r * 1.2f, center.y - r * 1.2f, + r * 1.2f * 2.0f, r * 1.2f * 2.0f); + + ege::setcolor(ege::WHITE); + for (int num = 1; num <= 12; ++num) + { + char str[8]; + ege::ege_point p = getpos(center, float(num * pi2 / 12), r); + sprintf(str, "%d", num); + ege::outtextxy((int)p.x, (int)p.y, str); + } + + time_t t_now; + time(&t_now); + tm* t = localtime(&t_now); + ege::setcolor(EGEARGB(0xff, 0x0, 0x0, 0xff)); + ege::setlinewidth(10.0f); + + char str[32]; + ege::ege_point p; + + float h = float(t->tm_hour + t->tm_min / 60.0); + p = getpos(center, float(h * pi2 / 12), r * 0.5f); + ege::ege_line(p.x, p.y, center.x, center.y); + + ege::setcolor(EGEARGB(0xff, 0xff, 0x0, 0xff)); + ege::setlinewidth(5.0f); + + float m = float(t->tm_min + t->tm_sec / 60.0); + p = getpos(center, float(m * pi2 / 60), r * 0.9f); + ege::ege_line(p.x, p.y, center.x, center.y); + + ege::setcolor(EGEARGB(0xff, 0xff, 0xff, 0)); + ege::setfillcolor(EGEARGB(0xff, 0xff, 0xff, 0)); + ege::setlinewidth(1.0f); + + float s = float(t->tm_sec); + p = getpos(center, float(s * pi2 / 60), r * 1.0f); + ege::ege_line(p.x, p.y, center.x, center.y); + ege::ege_fillellipse(center.x - r * 0.05f, center.y - r * 0.05f, + r * 0.1f, r * 0.1f); + + sprintf(str, "%d/%02d/%02d %2d:%02d:%02d", + t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, + t->tm_hour, t->tm_min, t->tm_sec); + ege::setcolor(EGERGB(0xff, 0xff, 0)); + ege::outtextxy((int)center.x, (int)(center.y + r * 1.4f), str); + +} + +/** + * @brief 主循环函数 + * + * 不断清空绘图设备并绘制时钟。 + */ +void mainloop() +{ + for (; ege::is_run(); ege::delay_fps(60)) + { + ege::cleardevice(); + draw(); + } +} + +/** + * @brief 主函数 + * + * 初始化绘图设备,设置初始模式为动画模式,创建一个400x480的图形窗口,然后进入主循环。 + * 主循环负责不断清空绘图设备并绘制时钟,直到用户关闭窗口。 + * 最后关闭绘图设备,结束程序。 + */ +int main() +{ + ege::setinitmode(ege::INIT_ANIMATION); + ege::initgraph(400, 480); + ege::randomize(); + mainloop(); + ege::closegraph(); + return 0; +} + diff --git a/cmake_template/ege_demos/graph_function_visualization.cpp b/cmake_template/ege_demos/graph_function_visualization.cpp new file mode 100644 index 0000000..34b5b45 --- /dev/null +++ b/cmake_template/ege_demos/graph_function_visualization.cpp @@ -0,0 +1,639 @@ +/** + * @file graph_function_visualization.cpp + * @author wysaid (this@xege.org) + * @brief 基于蒙特卡洛法的 2D 函数图像绘制 + * @version 0.1 + * @date 2025-07-12 + * + */ + +#ifndef NOMINMAX +#define NOMINMAX 1 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 文本本地化宏定义 +#ifdef _MSC_VER +// MSVC编译器使用中文文案 +#define TEXT_WINDOW_TITLE "EGE - 2D 函数图像绘制器 (蒙特卡洛法)" +#define TEXT_CONTROLS_TITLE "控制说明:" +#define TEXT_CONTROLS_SPACE "空格 - 切换到下一个函数" +#define TEXT_CONTROLS_SAMPLES "+/= - 增加采样点数量 (+10000)" +#define TEXT_CONTROLS_SAMPLES_DOWN "-/_ - 减少采样点数量 (-10000)" +#define TEXT_CONTROLS_TOLERANCE_UP "W - 增加容差值 (+0.01) - 使函数线条更粗" +#define TEXT_CONTROLS_TOLERANCE_DOWN "S - 减少容差值 (-0.01) - 使函数线条更细" +#define TEXT_CONTROLS_POINT_SIZE_DOWN "[ - 减少点大小" +#define TEXT_CONTROLS_POINT_SIZE_UP "] - 增大点大小" +#define TEXT_CONTROLS_REDRAW "R - 重新绘制当前函数" +#define TEXT_CONTROLS_EXIT "ESC - 退出" +#define TEXT_CURRENT_FUNCTION "当前函数: %s" +#define TEXT_COORD_RANGE "坐标范围: X:[%.2f, %.2f] Y:[%.2f, %.2f]" +#define TEXT_PARAMS_INFO "采样点数: %d 容差: %.4f" +#define TEXT_POINT_SIZE_INFO "点大小: %d" +#define TEXT_FUNCTION_CIRCLE "圆形 (r=2)" +#define TEXT_FUNCTION_ELLIPSE "椭圆 (a=3, b=2)" +#define TEXT_FUNCTION_PARABOLA "抛物线 (y=x^2)" +#define TEXT_FUNCTION_HYPERBOLA "双曲线" +#define TEXT_FUNCTION_SINE "正弦波" +#define TEXT_FUNCTION_ROSE "玫瑰花 (n=3)" +#define TEXT_FUNCTION_HEART "心形" +#define TEXT_FUNCTION_LOTUS "莲花" +#define TEXT_FONT_NAME "宋体" +#else +// 非MSVC编译器使用英文文案 +#define TEXT_WINDOW_TITLE "EGE - 2D Function Graph Renderer (Monte Carlo Method)" +#define TEXT_CONTROLS_TITLE "Controls:" +#define TEXT_CONTROLS_SPACE "SPACE - Next function" +#define TEXT_CONTROLS_SAMPLES "+/= - Increase samples (+10000)" +#define TEXT_CONTROLS_SAMPLES_DOWN "-/_ - Decrease samples (-10000)" +#define TEXT_CONTROLS_TOLERANCE_UP "W - Increase tolerance (+0.01)" +#define TEXT_CONTROLS_TOLERANCE_DOWN "S - Decrease tolerance (-0.01)" +#define TEXT_CONTROLS_POINT_SIZE_DOWN "[ - Decrease point size" +#define TEXT_CONTROLS_POINT_SIZE_UP "] - Increase point size" +#define TEXT_CONTROLS_REDRAW "R - Redraw current function" +#define TEXT_CONTROLS_EXIT "ESC - Exit" +#define TEXT_CURRENT_FUNCTION "Current: %s" +#define TEXT_COORD_RANGE "X: [%.2f, %.2f] Y: [%.2f, %.2f]" +#define TEXT_PARAMS_INFO "Samples: %d Tolerance: %.4f" +#define TEXT_POINT_SIZE_INFO "Point Size: %d" +#define TEXT_FUNCTION_CIRCLE "Circle (r=2)" +#define TEXT_FUNCTION_ELLIPSE "Ellipse (a=3, b=2)" +#define TEXT_FUNCTION_PARABOLA "Parabola (y=x^2)" +#define TEXT_FUNCTION_HYPERBOLA "Hyperbola" +#define TEXT_FUNCTION_SINE "Sine Wave" +#define TEXT_FUNCTION_ROSE "Rose (n=3)" +#define TEXT_FUNCTION_HEART "Heart" +#define TEXT_FUNCTION_LOTUS "Lotus" +#define TEXT_FONT_NAME "Arial" +#endif + +/** + * @class Function2DRenderer + * @brief 基于蒙特卡洛法的2D函数图像绘制类 + * + * 该类可以绘制形如 f(x,y) = 0 的函数图像,通过蒙特卡洛方法 + * 在指定区域内随机采样点,计算函数值,当函数值接近0时绘制该点 + */ +class Function2DRenderer +{ +public: + /** + * @brief 函数类型定义 + * 函数应该返回 f(x,y) 的值,当返回值为0时表示该点在函数图像上 + */ + using FunctionType = std::function; + + /** + * @brief 构造函数 + * @param width 绘图区域宽度(像素) + * @param height 绘图区域高度(像素) + * @param xMin 数学坐标系X轴最小值 + * @param xMax 数学坐标系X轴最大值 + * @param yMin 数学坐标系Y轴最小值 + * @param yMax 数学坐标系Y轴最大值 + */ + Function2DRenderer(int width, int height, double xMin, double xMax, double yMin, double yMax) : + m_width(width), + m_height(height), + m_xMin(xMin), + m_xMax(xMax), + m_yMin(yMin), + m_yMax(yMax), + m_tolerance(0.01), + m_sampleCount(100000), + m_pointColor(RED), + m_backgroundColor(BLACK), + m_drawAxes(true), + m_axisColor(WHITE), + m_gridColor(DARKGRAY), + m_showGrid(true), + m_generator(std::random_device{}()), + m_xDistribution(m_xMin, m_xMax), + m_yDistribution(m_yMin, m_yMax) + {} + + /** + * @brief 设置函数容差 + * @param tolerance 容差值,当 |f(x,y)| < tolerance 时认为点在函数图像上 + */ + void setTolerance(double tolerance) { m_tolerance = std::abs(tolerance); } + + double tolerance() const { return m_tolerance; } + + /** + * @brief 设置采样点数量 + * @param count 蒙特卡洛采样的点数量 + */ + void setSampleCount(int count) { m_sampleCount = std::max(1000, count); } + + int sampleCount() const { return m_sampleCount; } + + /** + * @brief 设置绘图颜色 + * @param pointColor 函数图像点的颜色 + * @param backgroundColor 背景颜色 + */ + void setColors(color_t pointColor, color_t backgroundColor) + { + m_pointColor = pointColor; + m_backgroundColor = backgroundColor; + } + + /** + * @brief 设置坐标轴颜色 + * @param axisColor 坐标轴颜色 + * @param gridColor 网格颜色 + */ + void setAxisColors(color_t axisColor, color_t gridColor) + { + m_axisColor = axisColor; + m_gridColor = gridColor; + } + + /** + * @brief 设置是否绘制坐标轴 + * @param drawAxes 是否绘制坐标轴 + */ + void setDrawAxes(bool drawAxes) { m_drawAxes = drawAxes; } + + /** + * @brief 设置是否显示网格 + * @param showGrid 是否显示网格 + */ + void setShowGrid(bool showGrid) { m_showGrid = showGrid; } + + /** + * @brief 设置点大小 + * @param size 点的大小(像素) + */ + void setPointSize(int size) { m_pointSize = std::max(1, std::min(20, size)); } + + int pointSize() const { return m_pointSize; } + + /** + * @brief 绘制函数图像 + * @param func 要绘制的函数 + * @param pimg 目标图像,NULL表示绘制到当前窗口 + */ + void render(const FunctionType& func, PIMAGE pimg = NULL) + { + if (!func) { + return; + } + + // 绘制网格 + if (m_showGrid) { + drawGrid(pimg); + } + + // 绘制坐标轴 + if (m_drawAxes) { + drawAxes(pimg); + } + + // 蒙特卡洛采样绘制函数图像 + drawFunction(func, pimg); + + // 绘制标签 + drawLabels(pimg); + } + + /** + * @brief 添加预定义函数示例 + * @param name 函数名称 + * @param func 函数对象 + */ + void addFunction(const std::string& name, const FunctionType& func) { m_functions[name] = func; } + + /** + * @brief 获取预定义函数列表 + * @return 函数名称列表 + */ + std::vector getFunctionNames() const + { + std::vector names; + for (const auto& pair : m_functions) { + names.push_back(pair.first); + } + return names; + } + + /** + * @brief 绘制预定义函数 + * @param name 函数名称 + * @param pimg 目标图像 + */ + void renderFunction(const std::string& name, PIMAGE pimg) + { + auto it = m_functions.find(name); + if (it != m_functions.end()) { + render(it->second, pimg); + } + } + +private: + /** + * @brief 将数学坐标转换为屏幕坐标 + * @param x 数学坐标x + * @param y 数学坐标y + * @param screenX 输出屏幕坐标x + * @param screenY 输出屏幕坐标y + */ + void mathToScreen(double x, double y, int& screenX, int& screenY) const + { + screenX = static_cast((x - m_xMin) / (m_xMax - m_xMin) * m_width); + screenY = static_cast((m_yMax - y) / (m_yMax - m_yMin) * m_height); + } + + /** + * @brief 绘制坐标轴 + * @param pimg 目标图像 + */ + void drawAxes(PIMAGE pimg) + { + setcolor(m_axisColor, pimg); + setlinewidth(2, pimg); + + // 绘制X轴 + if (m_yMin <= 0 && m_yMax >= 0) { + int screenX1, screenY1, screenX2, screenY2; + mathToScreen(m_xMin, 0, screenX1, screenY1); + mathToScreen(m_xMax, 0, screenX2, screenY2); + line(screenX1, screenY1, screenX2, screenY2, pimg); + } + + // 绘制Y轴 + if (m_xMin <= 0 && m_xMax >= 0) { + int screenX1, screenY1, screenX2, screenY2; + mathToScreen(0, m_yMin, screenX1, screenY1); + mathToScreen(0, m_yMax, screenX2, screenY2); + line(screenX1, screenY1, screenX2, screenY2, pimg); + } + } + + /** + * @brief 绘制网格 + * @param pimg 目标图像 + */ + void drawGrid(PIMAGE pimg) + { + setcolor(m_gridColor, pimg); + setlinewidth(1, pimg); + + // 计算网格间隔 + double xStep = (m_xMax - m_xMin) / 20.0; + double yStep = (m_yMax - m_yMin) / 15.0; + + // 绘制垂直网格线 + for (double x = m_xMin; x <= m_xMax; x += xStep) { + int screenX1, screenY1, screenX2, screenY2; + mathToScreen(x, m_yMin, screenX1, screenY1); + mathToScreen(x, m_yMax, screenX2, screenY2); + line(screenX1, screenY1, screenX2, screenY2, pimg); + } + + // 绘制水平网格线 + for (double y = m_yMin; y <= m_yMax; y += yStep) { + int screenX1, screenY1, screenX2, screenY2; + mathToScreen(m_xMin, y, screenX1, screenY1); + mathToScreen(m_xMax, y, screenX2, screenY2); + line(screenX1, screenY1, screenX2, screenY2, pimg); + } + } + + /** + * @brief 使用蒙特卡洛法绘制函数图像 + * @param func 要绘制的函数 + * @param pimg 目标图像 + */ + void drawFunction(const FunctionType& func, PIMAGE pimg) + { + setcolor(m_pointColor, pimg); + setfillcolor(m_pointColor, pimg); + + for (int i = 0; i < m_sampleCount; ++i) { + // 生成随机点 + double x = m_xDistribution(m_generator); + double y = m_yDistribution(m_generator); + + // 计算函数值 + double value = func(x, y); + + // 如果函数值在容差范围内,绘制该点 + if (std::abs(value) < m_tolerance) { + int screenX, screenY; + mathToScreen(x, y, screenX, screenY); + + // 检查是否在屏幕范围内 + if (screenX >= 0 && screenX < m_width && screenY >= 0 && screenY < m_height) { + if (m_pointSize == 1) { + putpixel(screenX, screenY, m_pointColor, pimg); + } else { + // 绘制圆形点 + fillcircle(screenX, screenY, m_pointSize, pimg); + } + } + } + } + } + + /** + * @brief 绘制标签和坐标 + * @param pimg 目标图像 + */ + void drawLabels(PIMAGE pimg) + { + setcolor(m_axisColor, pimg); + setbkmode(TRANSPARENT, pimg); + + // 绘制坐标范围信息 + if (pimg) { + // 如果指定了目标图像,需要临时设置为当前目标 + PIMAGE oldTarget = gettarget(); + settarget(pimg); + xyprintf(10, 10, TEXT_COORD_RANGE, m_xMin, m_xMax, m_yMin, m_yMax); + xyprintf(10, 30, TEXT_PARAMS_INFO, m_sampleCount, m_tolerance); + xyprintf(10, 50, TEXT_POINT_SIZE_INFO, m_pointSize); + settarget(oldTarget); + } else { + // 直接输出到当前窗口 + xyprintf(10, 10, TEXT_COORD_RANGE, m_xMin, m_xMax, m_yMin, m_yMax); + xyprintf(10, 30, TEXT_PARAMS_INFO, m_sampleCount, m_tolerance); + xyprintf(10, 50, TEXT_POINT_SIZE_INFO, m_pointSize); + } + } + +private: + int m_width, m_height; // 绘图区域尺寸 + double m_xMin, m_xMax; // X轴数学坐标范围 + double m_yMin, m_yMax; // Y轴数学坐标范围 + double m_tolerance; // 函数值容差 + int m_sampleCount; // 蒙特卡洛采样点数 + color_t m_pointColor; // 函数点颜色 + color_t m_backgroundColor; // 背景颜色 + color_t m_axisColor; // 坐标轴颜色 + color_t m_gridColor; // 网格颜色 + bool m_drawAxes; // 是否绘制坐标轴 + bool m_showGrid; // 是否显示网格 + int m_pointSize = 3; // 点大小 + + // 随机数生成器 + mutable std::mt19937 m_generator; + mutable std::uniform_real_distribution m_xDistribution; + mutable std::uniform_real_distribution m_yDistribution; + + // 预定义函数集合 + std::map m_functions; +}; + +// 示例函数定义 +namespace Examples +{ + +/** + * @brief 圆形函数: x^2 + y^2 - r^2 = 0 + * @param radius 圆的半径 + * @return 圆形函数 + */ +Function2DRenderer::FunctionType circle(double radius) +{ + return [radius](double x, double y) -> double { return x * x + y * y - radius * radius; }; +} + +/** + * @brief 椭圆函数: (x/a)^2 + (y/b)^2 - 1 = 0 + * @param a 椭圆x轴半径 + * @param b 椭圆y轴半径 + * @return 椭圆函数 + */ +Function2DRenderer::FunctionType ellipse(double a, double b) +{ + return [a, b](double x, double y) -> double { return (x * x) / (a * a) + (y * y) / (b * b) - 1.0; }; +} + +/** + * @brief 抛物线函数: y - ax^2 - bx - c = 0 + * @param a 二次项系数 + * @param b 一次项系数 + * @param c 常数项 + * @return 抛物线函数 + */ +Function2DRenderer::FunctionType parabola(double a, double b, double c) +{ + return [a, b, c](double x, double y) -> double { return y - a * x * x - b * x - c; }; +} + +/** + * @brief 双曲线函数: (x/a)^2 - (y/b)^2 - 1 = 0 + * @param a x轴参数 + * @param b y轴参数 + * @return 双曲线函数 + */ +Function2DRenderer::FunctionType hyperbola(double a, double b) +{ + return [a, b](double x, double y) -> double { return (x * x) / (a * a) - (y * y) / (b * b) - 1.0; }; +} + +/** + * @brief 正弦波函数: y - A*sin(B*x + C) = 0 + * @param A 振幅 + * @param B 频率 + * @param C 相位 + * @return 正弦波函数 + */ +Function2DRenderer::FunctionType sineWave(double A, double B, double C) +{ + return [A, B, C](double x, double y) -> double { return y - A * std::sin(B * x + C); }; +} + +/** + * @brief 花瓣函数: r = A*sin(n*θ), 转换为直角坐标 + * @param A 振幅 + * @param n 花瓣数量参数 + * @return 花瓣函数 + */ +Function2DRenderer::FunctionType rose(double A, int n) +{ + return [A, n](double x, double y) -> double { + double r = std::sqrt(x * x + y * y); + if (r < 1e-10) { + return 0.0; + } + double theta = std::atan2(y, x); + double expected_r = A * std::sin(n * theta); + return std::abs(r - std::abs(expected_r)); + }; +} + +/** + * @brief 心形函数: (x^2 + y^2 - 1)^3 - x^2*y^3 = 0 + * @return 心形函数 + */ +Function2DRenderer::FunctionType heart() +{ + return [](double x, double y) -> double { + double temp = x * x + y * y - 1.0; + return temp * temp * temp - x * x * y * y * y; + }; +} + +/** + * @brief 莲花函数: x^2 + y^2 - sin(4*atan2(y,x)) = 0 + * @return 莲花函数 + */ +Function2DRenderer::FunctionType lotus() +{ + return [](double x, double y) -> double { + double r = std::sqrt(x * x + y * y); + double theta = std::atan2(y, x); + return r - 0.5 * (1 + std::sin(4 * theta)); + }; +} +} // namespace Examples + +// 演示程序 +int main() +{ + // 初始化图形界面 + initgraph(800, 600, INIT_RENDERMANUAL); + setbkcolor(BLACK); + setbkmode(TRANSPARENT); + setcaption(TEXT_WINDOW_TITLE); + + // 设置字体 + settextjustify(LEFT_TEXT, TOP_TEXT); + setfont(16, 0, TEXT_FONT_NAME); + + // 创建函数图像渲染器 + Function2DRenderer renderer(800, 600, -5.0, 5.0, -5.0, 5.0); + + // 设置渲染参数 + renderer.setTolerance(0.05); + renderer.setSampleCount(200000); + renderer.setColors(YELLOW, BLACK); + renderer.setAxisColors(WHITE, DARKGRAY); + renderer.setDrawAxes(true); + renderer.setShowGrid(true); + + // 添加预定义函数 + renderer.addFunction(TEXT_FUNCTION_CIRCLE, Examples::circle(2.0)); + renderer.addFunction(TEXT_FUNCTION_ELLIPSE, Examples::ellipse(3.0, 2.0)); + renderer.addFunction(TEXT_FUNCTION_PARABOLA, Examples::parabola(1.0, 0.0, 0.0)); + renderer.addFunction(TEXT_FUNCTION_HYPERBOLA, Examples::hyperbola(2.0, 1.5)); + renderer.addFunction(TEXT_FUNCTION_SINE, Examples::sineWave(2.0, 1.0, 0.0)); + renderer.addFunction(TEXT_FUNCTION_ROSE, Examples::rose(2.0, 3)); + renderer.addFunction(TEXT_FUNCTION_HEART, Examples::heart()); + renderer.addFunction(TEXT_FUNCTION_LOTUS, Examples::lotus()); + + // 获取函数列表 + auto functionNames = renderer.getFunctionNames(); + int currentFunction = 0; + + PIMAGE imgCache = newimage(800, 600); + + // 渲染第一个函数 + if (!functionNames.empty()) { + renderer.renderFunction(functionNames[currentFunction], imgCache); + } + + // 主循环 + bool running = true; + bool drawFunction = true; + + while (running) { + // 检查键盘输入 + + while (kbhit()) { + int key = getch(); + + switch (key) { + case 27: // ESC键 - 退出程序 + running = false; + break; + case '+': // 增加采样点数量 (+10000) + case '=': + renderer.setSampleCount(renderer.sampleCount() + 10000); + drawFunction = true; + break; + case '-': // 减少采样点数量 (-10000) + case '_': + renderer.setSampleCount(std::max(1000, renderer.sampleCount() - 10000)); + drawFunction = true; + break; + case 'w': // 增加容差值 (+0.01) - 使函数线条更粗 + case 'W': + renderer.setTolerance(std::min(1.0, renderer.tolerance() + 0.01)); + drawFunction = true; + break; + case 's': // 减少容差值 (-0.01) - 使函数线条更细 + case 'S': + renderer.setTolerance(std::max(0.01, renderer.tolerance() - 0.01)); + drawFunction = true; + break; + case '[': // 减少点大小 + renderer.setPointSize(renderer.pointSize() - 1); + drawFunction = true; + break; + case ']': // 增大点大小 + renderer.setPointSize(renderer.pointSize() + 1); + drawFunction = true; + break; + case 32: // 空格键 - 切换到下一个函数 + if (!functionNames.empty()) { + currentFunction = (currentFunction + 1) % functionNames.size(); + drawFunction = true; + } + break; + case 'r': // R键 - 重新绘制当前函数 + case 'R': + drawFunction = true; + break; + } + } + + if (drawFunction && !functionNames.empty()) { + settarget(imgCache); + setbkcolor(BLACK); + cleardevice(); // 清除屏幕 + setcolor(WHITE); + setbkmode(TRANSPARENT); + outtextxy(10, 50, TEXT_CONTROLS_TITLE, NULL); + outtextxy(10, 70, TEXT_CONTROLS_SPACE, NULL); + outtextxy(10, 90, TEXT_CONTROLS_SAMPLES, NULL); + outtextxy(10, 110, TEXT_CONTROLS_SAMPLES_DOWN, NULL); + outtextxy(10, 130, TEXT_CONTROLS_TOLERANCE_UP, NULL); + outtextxy(10, 150, TEXT_CONTROLS_TOLERANCE_DOWN, NULL); + outtextxy(10, 170, TEXT_CONTROLS_POINT_SIZE_DOWN, NULL); + outtextxy(10, 190, TEXT_CONTROLS_POINT_SIZE_UP, NULL); + outtextxy(10, 210, TEXT_CONTROLS_REDRAW, NULL); + outtextxy(10, 230, TEXT_CONTROLS_EXIT, NULL); + + // 显示当前函数名称 + xyprintf(10, 250, TEXT_CURRENT_FUNCTION, functionNames[currentFunction].c_str()); + + renderer.renderFunction(functionNames[currentFunction], imgCache); + settarget(nullptr); + drawFunction = false; // 重置绘制标志 + } + + cleardevice(); // 清除屏幕 + putimage(0, 0, imgCache); // 绘制缓存图像 + + delay_fps(60); + } + + // 清理资源 + delimage(imgCache); + closegraph(); + + return 0; +} \ No newline at end of file diff --git a/cmake_template/ege_demos/graph_game_of_life.cpp b/cmake_template/ege_demos/graph_game_of_life.cpp new file mode 100644 index 0000000..e89c2dd --- /dev/null +++ b/cmake_template/ege_demos/graph_game_of_life.cpp @@ -0,0 +1,749 @@ +/** + * @file graph_game_of_life.cpp + * @brief Conway's Game of Life Visualization Demo + * + * 康威生命游戏可视化演示 + * - 经典的元胞自动机模拟 + * - 简单规则产生复杂涌现行为 + * - 支持鼠标绘制初始状态 + * - 内置多种经典图案 + * + * Controls / 控制: + * Space - Pause/Resume 暂停/继续 + * R - Random reset 随机重置 + * C - Clear grid 清空网格 + * G - Toggle grid lines 开关网格线 + * T - Toggle trail effect 开关轨迹效果 + * +/- - Speed up/down 加速/减速 + * 1-9 - Load preset patterns 加载预设图案 + * Mouse Left - Draw/Erase cells 绘制/擦除细胞 + * Mouse Right - Pan view 平移视图 + * Mouse Wheel - Zoom in/out 缩放 + * ESC - Exit 退出 + * + * Rules / 规则: + * 1. 任何活细胞周围少于2个活邻居则死亡(孤独) + * 2. 任何活细胞周围有2-3个活邻居则存活 + * 3. 任何活细胞周围超过3个活邻居则死亡(拥挤) + * 4. 任何死细胞周围恰好3个活邻居则复活(繁殖) + */ + +// 禁用 Windows.h 中的 min/max 宏,避免与 std::min/std::max 冲突 +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _MSC_VER +#define DEMO_TITLE "康威生命游戏" +#define STR_PAUSED "已暂停" +#define STR_RUNNING "运行中" +#define STR_GENERATION "代数: %d" +#define STR_POPULATION "存活: %d" +#define STR_SPEED "速度: %d 代/秒" +#define STR_ZOOM "缩放: %.1fx" +#define STR_GRID "网格: %s" +#define STR_TRAIL "轨迹: %s" +#define STR_ON "开" +#define STR_OFF "关" +#define STR_CONTROLS "控制说明" +#define STR_SPACE "空格 - 暂停/继续" +#define STR_KEY_R "R - 随机重置" +#define STR_KEY_C "C - 清空网格" +#define STR_KEY_G "G - 开关网格" +#define STR_KEY_T "T - 开关轨迹" +#define STR_PLUS_MINUS "+/- - 调整速度" +#define STR_MOUSE_L "左键 - 绘制细胞" +#define STR_MOUSE_R "右键 - 平移视图" +#define STR_WHEEL "滚轮 - 缩放" +#define STR_NUM_KEYS "1-9 - 预设图案" +#define STR_ESC "ESC - 退出" +#define STR_PATTERNS "预设图案" +#define STR_PAT_GLIDER "1 - 滑翔机" +#define STR_PAT_LWSS "2 - 轻型飞船" +#define STR_PAT_PULSAR "3 - 脉冲星" +#define STR_PAT_GOSPER "4 - 高斯帕枪" +#define STR_PAT_PENTA "5 - 五联体" +#define STR_PAT_DIEHARD "6 - 顽固" +#define STR_PAT_ACORN "7 - 橡子" +#define STR_PAT_INF "8 - 无限增长" +#define STR_PAT_RANDOM "9 - 随机" +#else +#define DEMO_TITLE "Conway's Game of Life" +#define STR_PAUSED "Paused" +#define STR_RUNNING "Running" +#define STR_GENERATION "Gen: %d" +#define STR_POPULATION "Pop: %d" +#define STR_SPEED "Speed: %d gen/s" +#define STR_ZOOM "Zoom: %.1fx" +#define STR_GRID "Grid: %s" +#define STR_TRAIL "Trail: %s" +#define STR_ON "On" +#define STR_OFF "Off" +#define STR_CONTROLS "Controls" +#define STR_SPACE "Space - Pause/Resume" +#define STR_KEY_R "R - Random Reset" +#define STR_KEY_C "C - Clear Grid" +#define STR_KEY_G "G - Toggle Grid" +#define STR_KEY_T "T - Toggle Trail" +#define STR_PLUS_MINUS "+/- - Adjust Speed" +#define STR_MOUSE_L "LMB - Draw Cells" +#define STR_MOUSE_R "RMB - Pan View" +#define STR_WHEEL "Wheel - Zoom" +#define STR_NUM_KEYS "1-9 - Preset Patterns" +#define STR_ESC "ESC - Exit" +#define STR_PATTERNS "Patterns" +#define STR_PAT_GLIDER "1 - Glider" +#define STR_PAT_LWSS "2 - LWSS" +#define STR_PAT_PULSAR "3 - Pulsar" +#define STR_PAT_GOSPER "4 - Gosper Gun" +#define STR_PAT_PENTA "5 - Pentadecathlon" +#define STR_PAT_DIEHARD "6 - Diehard" +#define STR_PAT_ACORN "7 - Acorn" +#define STR_PAT_INF "8 - Infinite Growth" +#define STR_PAT_RANDOM "9 - Random" +#endif + +using namespace ege; + +// Window dimensions +const int WINDOW_WIDTH = 1280; +const int WINDOW_HEIGHT = 800; +const int PANEL_WIDTH = 200; +const int GRID_AREA_WIDTH = WINDOW_WIDTH - PANEL_WIDTH; + +// Colors +const color_t LIFE_COLOR_BG = EGERGB(20, 20, 30); +const color_t LIFE_COLOR_PANEL = EGERGB(40, 40, 50); +const color_t LIFE_COLOR_TEXT = EGERGB(220, 220, 220); +const color_t LIFE_COLOR_TITLE = EGERGB(100, 200, 255); +const color_t LIFE_COLOR_GRID = EGERGB(50, 50, 60); +const color_t LIFE_COLOR_CELL_ALIVE = EGERGB(50, 255, 100); +const color_t LIFE_COLOR_CELL_BORN = EGERGB(100, 255, 150); +const color_t LIFE_COLOR_CELL_DYING = EGERGB(150, 100, 50); +const color_t LIFE_COLOR_TRAIL = EGERGB(30, 60, 40); + +// Grid settings +const int GRID_WIDTH = 200; +const int GRID_HEIGHT = 150; + +class GameOfLife +{ +public: + GameOfLife() : + m_paused(true), + m_showGrid(true), + m_showTrail(false), + m_generation(0), + m_population(0), + m_speed(10), + m_cellSize(6.0f), + m_offsetX(0.0f), + m_offsetY(0.0f), + m_isDragging(false), + m_isDrawing(false), + m_drawValue(true), + m_lastMouseX(0), + m_lastMouseY(0), + m_frameCount(0) + { + m_grid.resize(GRID_WIDTH * GRID_HEIGHT, false); + m_nextGrid.resize(GRID_WIDTH * GRID_HEIGHT, false); + m_trailGrid.resize(GRID_WIDTH * GRID_HEIGHT, 0); + m_prevGrid.resize(GRID_WIDTH * GRID_HEIGHT, false); + + // Center the view + m_offsetX = (GRID_AREA_WIDTH - GRID_WIDTH * m_cellSize) / 2; + m_offsetY = (WINDOW_HEIGHT - GRID_HEIGHT * m_cellSize) / 2; + + // Load default pattern (Gosper Glider Gun) + loadPattern(4); + } + + void run() + { + initgraph(WINDOW_WIDTH, WINDOW_HEIGHT); + setbkmode(TRANSPARENT); + setcaption(DEMO_TITLE); + setbkcolor(LIFE_COLOR_BG); + + while (is_run()) { + handleInput(); + + if (!m_paused) { + m_frameCount++; + int updateInterval = 60 / m_speed; + if (updateInterval < 1) { + updateInterval = 1; + } + + if (m_frameCount >= updateInterval) { + update(); + m_frameCount = 0; + } + } + + render(); + delay_fps(60); + } + + closegraph(); + } + +private: + // Grid access helpers + bool getCell(int x, int y) const + { + if (x < 0 || x >= GRID_WIDTH || y < 0 || y >= GRID_HEIGHT) { + return false; + } + return m_grid[y * GRID_WIDTH + x]; + } + + void setCell(int x, int y, bool alive) + { + if (x >= 0 && x < GRID_WIDTH && y >= 0 && y < GRID_HEIGHT) { + m_grid[y * GRID_WIDTH + x] = alive; + } + } + + int countNeighbors(int x, int y) const + { + int count = 0; + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) { + continue; + } + if (getCell(x + dx, y + dy)) { + count++; + } + } + } + return count; + } + + void update() + { + // Save previous state for visual effects + m_prevGrid = m_grid; + + m_population = 0; + + for (int y = 0; y < GRID_HEIGHT; y++) { + for (int x = 0; x < GRID_WIDTH; x++) { + int neighbors = countNeighbors(x, y); + bool alive = getCell(x, y); + bool nextState = false; + + if (alive) { + // Rule 1 & 3: Die if < 2 or > 3 neighbors + // Rule 2: Survive if 2 or 3 neighbors + nextState = (neighbors == 2 || neighbors == 3); + } else { + // Rule 4: Birth if exactly 3 neighbors + nextState = (neighbors == 3); + } + + m_nextGrid[y * GRID_WIDTH + x] = nextState; + + if (nextState) { + m_population++; + } + + // Update trail + if (m_showTrail) { + if (alive) { + m_trailGrid[y * GRID_WIDTH + x] = 255; + } else if (m_trailGrid[y * GRID_WIDTH + x] > 0) { + m_trailGrid[y * GRID_WIDTH + x] = std::max(0, m_trailGrid[y * GRID_WIDTH + x] - 15); + } + } + } + } + + std::swap(m_grid, m_nextGrid); + m_generation++; + } + + void render() + { + cleardevice(); + + // Draw trail effect + if (m_showTrail) { + for (int y = 0; y < GRID_HEIGHT; y++) { + for (int x = 0; x < GRID_WIDTH; x++) { + int trail = m_trailGrid[y * GRID_WIDTH + x]; + if (trail > 0 && !m_grid[y * GRID_WIDTH + x]) { + float screenX = m_offsetX + x * m_cellSize; + float screenY = m_offsetY + y * m_cellSize; + + if (screenX + m_cellSize >= 0 && screenX < GRID_AREA_WIDTH && screenY + m_cellSize >= 0 && + screenY < WINDOW_HEIGHT) + { + int r = 30 * trail / 255; + int g = 60 * trail / 255; + int b = 40 * trail / 255; + setfillcolor(EGERGB(r, g, b)); + bar((int)screenX, (int)screenY, (int)(screenX + m_cellSize - 1), + (int)(screenY + m_cellSize - 1)); + } + } + } + } + } + + // Draw grid lines + if (m_showGrid && m_cellSize >= 4) { + setcolor(LIFE_COLOR_GRID); + for (int x = 0; x <= GRID_WIDTH; x++) { + float screenX = m_offsetX + x * m_cellSize; + if (screenX >= 0 && screenX < GRID_AREA_WIDTH) { + line((int)screenX, 0, (int)screenX, WINDOW_HEIGHT); + } + } + for (int y = 0; y <= GRID_HEIGHT; y++) { + float screenY = m_offsetY + y * m_cellSize; + if (screenY >= 0 && screenY < WINDOW_HEIGHT) { + line(0, (int)screenY, GRID_AREA_WIDTH, (int)screenY); + } + } + } + + // Draw cells + for (int y = 0; y < GRID_HEIGHT; y++) { + for (int x = 0; x < GRID_WIDTH; x++) { + bool alive = m_grid[y * GRID_WIDTH + x]; + bool wasAlive = m_prevGrid[y * GRID_WIDTH + x]; + + if (alive || (wasAlive && !alive && !m_paused)) { + float screenX = m_offsetX + x * m_cellSize; + float screenY = m_offsetY + y * m_cellSize; + + // Culling + if (screenX + m_cellSize < 0 || screenX > GRID_AREA_WIDTH || screenY + m_cellSize < 0 || + screenY > WINDOW_HEIGHT) + { + continue; + } + + color_t cellColor; + if (alive && !wasAlive) { + // Just born + cellColor = LIFE_COLOR_CELL_BORN; + } else if (!alive && wasAlive) { + // Just died + cellColor = LIFE_COLOR_CELL_DYING; + } else { + // Alive + cellColor = LIFE_COLOR_CELL_ALIVE; + } + + setfillcolor(cellColor); + + float margin = m_cellSize >= 6 ? 1.0f : 0.0f; + bar((int)(screenX + margin), (int)(screenY + margin), (int)(screenX + m_cellSize - margin - 1), + (int)(screenY + m_cellSize - margin - 1)); + } + } + } + + // Draw panel + drawPanel(); + } + + void drawPanel() + { + // Panel background + setfillcolor(LIFE_COLOR_PANEL); + bar(GRID_AREA_WIDTH, 0, WINDOW_WIDTH, WINDOW_HEIGHT); + + // Border + setcolor(EGERGB(80, 80, 90)); + line(GRID_AREA_WIDTH, 0, GRID_AREA_WIDTH, WINDOW_HEIGHT); + + int x = GRID_AREA_WIDTH + 15; + int y = 20; + char buf[64]; + + // Title + setfont(24, 0, "Consolas"); + setcolor(LIFE_COLOR_TITLE); + outtextxy(x, y, DEMO_TITLE); + y += 40; + + // Status + setfont(16, 0, "Consolas"); + setcolor(m_paused ? EGERGB(255, 150, 100) : EGERGB(100, 255, 150)); + outtextxy(x, y, m_paused ? STR_PAUSED : STR_RUNNING); + y += 30; + + // Stats + setcolor(LIFE_COLOR_TEXT); + sprintf(buf, STR_GENERATION, m_generation); + outtextxy(x, y, buf); + y += 22; + + sprintf(buf, STR_POPULATION, m_population); + outtextxy(x, y, buf); + y += 22; + + sprintf(buf, STR_SPEED, m_speed); + outtextxy(x, y, buf); + y += 22; + + sprintf(buf, STR_ZOOM, m_cellSize / 6.0f); + outtextxy(x, y, buf); + y += 22; + + sprintf(buf, STR_GRID, m_showGrid ? STR_ON : STR_OFF); + outtextxy(x, y, buf); + y += 22; + + sprintf(buf, STR_TRAIL, m_showTrail ? STR_ON : STR_OFF); + outtextxy(x, y, buf); + y += 40; + + // Controls + setcolor(LIFE_COLOR_TITLE); + outtextxy(x, y, STR_CONTROLS); + y += 25; + + setfont(14, 0, "Consolas"); + setcolor(EGERGB(180, 180, 180)); + + outtextxy(x, y, STR_SPACE); + y += 18; + outtextxy(x, y, STR_KEY_R); + y += 18; + outtextxy(x, y, STR_KEY_C); + y += 18; + outtextxy(x, y, STR_KEY_G); + y += 18; + outtextxy(x, y, STR_KEY_T); + y += 18; + outtextxy(x, y, STR_PLUS_MINUS); + y += 18; + outtextxy(x, y, STR_MOUSE_L); + y += 18; + outtextxy(x, y, STR_MOUSE_R); + y += 18; + outtextxy(x, y, STR_WHEEL); + y += 18; + outtextxy(x, y, STR_NUM_KEYS); + y += 18; + outtextxy(x, y, STR_ESC); + y += 35; + + // Patterns + setfont(16, 0, "Consolas"); + setcolor(LIFE_COLOR_TITLE); + outtextxy(x, y, STR_PATTERNS); + y += 25; + + setfont(14, 0, "Consolas"); + setcolor(EGERGB(180, 180, 180)); + + outtextxy(x, y, STR_PAT_GLIDER); + y += 18; + outtextxy(x, y, STR_PAT_LWSS); + y += 18; + outtextxy(x, y, STR_PAT_PULSAR); + y += 18; + outtextxy(x, y, STR_PAT_GOSPER); + y += 18; + outtextxy(x, y, STR_PAT_PENTA); + y += 18; + outtextxy(x, y, STR_PAT_DIEHARD); + y += 18; + outtextxy(x, y, STR_PAT_ACORN); + y += 18; + outtextxy(x, y, STR_PAT_INF); + y += 18; + outtextxy(x, y, STR_PAT_RANDOM); + } + + void handleInput() + { + // Keyboard input + while (kbhit()) { + int key = getch(); + switch (key) { + case ' ': + m_paused = !m_paused; + break; + + case 'r': + case 'R': + randomize(); + break; + + case 'c': + case 'C': + clear(); + break; + + case 'g': + case 'G': + m_showGrid = !m_showGrid; + break; + + case 't': + case 'T': + m_showTrail = !m_showTrail; + if (!m_showTrail) { + std::fill(m_trailGrid.begin(), m_trailGrid.end(), 0); + } + break; + + case '+': + case '=': + m_speed = std::min(60, m_speed + 5); + break; + + case '-': + case '_': + m_speed = std::max(1, m_speed - 5); + break; + + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + loadPattern(key - '0'); + break; + + case key_esc: + closegraph(); + exit(0); + break; + } + } + + // Mouse input + while (mousemsg()) { + mouse_msg msg = getmouse(); + + if (msg.x < GRID_AREA_WIDTH) { + if (msg.is_left()) { + if (msg.is_down()) { + m_isDrawing = true; + int gridX = (int)((msg.x - m_offsetX) / m_cellSize); + int gridY = (int)((msg.y - m_offsetY) / m_cellSize); + if (gridX >= 0 && gridX < GRID_WIDTH && gridY >= 0 && gridY < GRID_HEIGHT) { + m_drawValue = !getCell(gridX, gridY); + setCell(gridX, gridY, m_drawValue); + updatePopulation(); + } + } else if (msg.is_up()) { + m_isDrawing = false; + } else if (msg.is_move() && m_isDrawing) { + int gridX = (int)((msg.x - m_offsetX) / m_cellSize); + int gridY = (int)((msg.y - m_offsetY) / m_cellSize); + if (gridX >= 0 && gridX < GRID_WIDTH && gridY >= 0 && gridY < GRID_HEIGHT) { + setCell(gridX, gridY, m_drawValue); + updatePopulation(); + } + } + } + + if (msg.is_right()) { + if (msg.is_down()) { + m_isDragging = true; + m_lastMouseX = msg.x; + m_lastMouseY = msg.y; + } else if (msg.is_up()) { + m_isDragging = false; + } else if (msg.is_move() && m_isDragging) { + m_offsetX += msg.x - m_lastMouseX; + m_offsetY += msg.y - m_lastMouseY; + m_lastMouseX = msg.x; + m_lastMouseY = msg.y; + } + } + + if (msg.is_wheel()) { + float oldCellSize = m_cellSize; + float zoomFactor = msg.wheel > 0 ? 1.2f : 0.8f; + m_cellSize *= zoomFactor; + m_cellSize = std::max(2.0f, std::min(30.0f, m_cellSize)); + + // Zoom towards mouse position + float mouseGridX = (msg.x - m_offsetX) / oldCellSize; + float mouseGridY = (msg.y - m_offsetY) / oldCellSize; + m_offsetX = msg.x - mouseGridX * m_cellSize; + m_offsetY = msg.y - mouseGridY * m_cellSize; + } + } + } + } + + void updatePopulation() + { + m_population = 0; + for (int i = 0; i < GRID_WIDTH * GRID_HEIGHT; i++) { + if (m_grid[i]) { + m_population++; + } + } + } + + void clear() + { + std::fill(m_grid.begin(), m_grid.end(), false); + std::fill(m_trailGrid.begin(), m_trailGrid.end(), 0); + std::fill(m_prevGrid.begin(), m_prevGrid.end(), false); + m_generation = 0; + m_population = 0; + } + + void randomize() + { + clear(); + for (int y = 0; y < GRID_HEIGHT; y++) { + for (int x = 0; x < GRID_WIDTH; x++) { + if (rand() % 100 < 25) { + setCell(x, y, true); + m_population++; + } + } + } + } + + void loadPattern(int patternIndex) + { + clear(); + + int centerX = GRID_WIDTH / 2; + int centerY = GRID_HEIGHT / 2; + + // Pattern definitions (relative coordinates) + std::vector> pattern; + + switch (patternIndex) { + case 1: // Glider + pattern = {{0, 0}, {1, 0}, {2, 0}, {2, -1}, {1, -2}}; + break; + + case 2: // Lightweight Spaceship (LWSS) + pattern = {{0, 0}, {3, 0}, {4, 1}, {0, 2}, {4, 2}, {1, 3}, {2, 3}, {3, 3}, {4, 3}}; + break; + + case 3: // Pulsar + pattern = { + // Top left quadrant pattern (will be mirrored) + {2, 0}, + {3, 0}, + {4, 0}, + {0, 2}, + {5, 2}, + {0, 3}, + {5, 3}, + {0, 4}, + {5, 4}, + {2, 5}, + {3, 5}, + {4, 5}, + }; + // Mirror the pattern + { + std::vector> fullPattern; + for (auto& p : pattern) { + fullPattern.push_back({p.first, p.second}); + fullPattern.push_back({-p.first - 1, p.second}); + fullPattern.push_back({p.first, -p.second - 1}); + fullPattern.push_back({-p.first - 1, -p.second - 1}); + } + pattern = fullPattern; + } + break; + + case 4: // Gosper Glider Gun + pattern = {{0, 4}, {0, 5}, {1, 4}, {1, 5}, {10, 4}, {10, 5}, {10, 6}, {11, 3}, {11, 7}, {12, 2}, {12, 8}, + {13, 2}, {13, 8}, {14, 5}, {15, 3}, {15, 7}, {16, 4}, {16, 5}, {16, 6}, {17, 5}, {20, 2}, {20, 3}, + {20, 4}, {21, 2}, {21, 3}, {21, 4}, {22, 1}, {22, 5}, {24, 0}, {24, 1}, {24, 5}, {24, 6}, {34, 2}, + {34, 3}, {35, 2}, {35, 3}}; + centerX = GRID_WIDTH / 4; + centerY = GRID_HEIGHT / 2; + break; + + case 5: // Pentadecathlon + pattern = { + {-4, 0}, {-3, 0}, {-2, -1}, {-2, 1}, {-1, 0}, {0, 0}, {1, 0}, {2, 0}, {3, -1}, {3, 1}, {4, 0}, {5, 0}}; + break; + + case 6: // Diehard + pattern = {{0, 0}, {1, 0}, {1, 1}, {5, 1}, {6, -1}, {6, 1}, {7, 1}}; + break; + + case 7: // Acorn + pattern = {{0, 0}, {1, -2}, {1, 0}, {3, -1}, {4, 0}, {5, 0}, {6, 0}}; + break; + + case 8: // Infinite growth (R-pentomino) + pattern = {{0, 0}, {1, 0}, {0, 1}, {-1, 1}, {0, 2}}; + break; + + case 9: // Random + randomize(); + return; + } + + // Place pattern + for (auto& p : pattern) { + setCell(centerX + p.first, centerY + p.second, true); + } + + updatePopulation(); + + // Center view on pattern + m_offsetX = (GRID_AREA_WIDTH - GRID_WIDTH * m_cellSize) / 2; + m_offsetY = (WINDOW_HEIGHT - GRID_HEIGHT * m_cellSize) / 2; + } + + std::vector m_grid; + std::vector m_nextGrid; + std::vector m_prevGrid; + std::vector m_trailGrid; + + bool m_paused; + bool m_showGrid; + bool m_showTrail; + int m_generation; + int m_population; + int m_speed; + float m_cellSize; + float m_offsetX; + float m_offsetY; + + bool m_isDragging; + bool m_isDrawing; + bool m_drawValue; + int m_lastMouseX; + int m_lastMouseY; + int m_frameCount; +}; + +int main() +{ + srand((unsigned)time(NULL)); + setinitmode(INIT_ANIMATION); + + GameOfLife game; + game.run(); + + return 0; +} diff --git a/cmake_template/ege_demos/graph_getimage.cpp b/cmake_template/ege_demos/graph_getimage.cpp new file mode 100644 index 0000000..d42a31b --- /dev/null +++ b/cmake_template/ege_demos/graph_getimage.cpp @@ -0,0 +1,64 @@ +#include +#include +#include + +#define MSG_LEN 200 + +/** + * @brief 主函数 + * + * 这个程序演示了使用图形库中的getimage函数读取和显示PNG和JPG图像文件。 + * 它首先读取一个PNG文件(getimage.png),并使用putimage_withalpha函数在坐标(50, 50)处以带有alpha透明度的方式显示图像。 + * 然后,它读取一个JPG文件(getimage.jpg),并使用putimage函数在坐标(200, 50)处显示图像。 + * 最后,它尝试读取一个不存在的PNG文件(getimage1.png),如果读取失败,则显示错误消息。 + */ +int main() +{ + char msg[MSG_LEN+1]; + PIMAGE img; + int result; + initgraph( 640, 480 ); + setbkcolor(WHITE); + + // @note 读取一个PNG文件(getimage.png),并使用putimage_withalpha函数在坐标(50, 50)处以带有alpha透明度的方式显示图像 + img = newimage(); + result = getimage(img, "getimage.png"); + if (result != grOk) { + sprintf(msg, "getimage(png) failed with %d.", result); + outtextxy(0, 440, msg); + getch(); + exit(-1); + } + putimage_withalpha(NULL, img, 50, 50); + putimage_withalpha(NULL, img, 50, 200, 150, 150, 0, 0, getwidth(img), getheight(img)); + delimage(img); + + // @note 读取一个JPG文件(getimage.jpg),并使用putimage函数在坐标(200, 50)处显示图像。 + img = newimage(); + result = getimage(img, "getimage.jpg"); + if (result != grOk) { + sprintf(msg, "getimage(jpg) failed with %d.", result); + outtextxy(0, 440, msg); + getch(); + exit(-1); + } + putimage(200, 50, img); + delimage(img); + + // @note 尝试读取一个不存在的PNG文件(getimage1.png),如果读取失败,则显示错误消息。 + img = newimage(); + result = getimage(img, "getimage1.png"); + if (result != grOk) { + sprintf(msg, "getimage(png) failed with %d.", result); + outtextxy(0, 440, msg); + getch(); + exit(-1); + } + putimage(50, 50, img); + delimage(img); + + getch(); + closegraph(); + return 0; +} + diff --git a/cmake_template/ege_demos/graph_julia.cpp b/cmake_template/ege_demos/graph_julia.cpp new file mode 100644 index 0000000..8b584cd --- /dev/null +++ b/cmake_template/ege_demos/graph_julia.cpp @@ -0,0 +1,635 @@ +/** + * @file egejulia.cpp + * + * @brief Julia集计算屏保动画 + * (编译后改为scr后缀使用) + */ + +#include "graphics.h" +#include +#include +#include +#include + +// 定义常量 +#define MAXCOLOR 64 // 颜色数 +#define BF_W 1200 +#define BF_H 1200 + +int g_w, g_h; + +///////////////////////////////////////////////// +// 定义复数及乘、加运算 +///////////////////////////////////////////////// + +/** + * @brief 复数结构体 + */ +struct COMPLEX +{ + double re; /**< 实部 */ + double im; /**< 虚部 */ +}; + +/** + * @brief 复数乘法运算符重载 + * @param a 复数a + * @param b 复数b + * @return 两个复数的乘积 + */ +COMPLEX operator * (COMPLEX a, COMPLEX b) +{ + COMPLEX c; + c.re = a.re * b.re - a.im * b.im; + c.im = a.im * b.re + a.re * b.im; + return c; +} + +/** + * @brief 复数加法运算符重载 + * @param a 复数a + * @param b 复数b + * @return 两个复数的和 + */ +COMPLEX operator + (COMPLEX a, COMPLEX b) +{ + COMPLEX c; + c.re = a.re + b.re; + c.im = a.im + b.im; + return c; +} + + +///////////////////////////////////////////////// +// 定义颜色及初始化颜色 +///////////////////////////////////////////////// + +int Color[MAXCOLOR]; /**< 颜色数组 */ + +/** + * @brief 初始化颜色 + */ +void InitColor() +{ + // 使用 HSL 颜色模式产生角度 h1 到 h2 的渐变色 + int h1 = 240, h2 = 30; + for(int i=0; i= BF_W || y < 0 || y >= BF_H) return; + if (pMap[y][x].ed == 0) + { + g_udlist.push(x, y); + } +} + +/** + * @brief 添加一个点到Julia集更新列表 + * @param x x坐标 + * @param y y坐标 + * @param it 迭代次数 + */ +void jaddpoint(int x, int y, int it = -1) +{ + if (x < 0 || x >= g_w || y < 0 || y >= g_h) return; + if (g_st[y * g_w + x].ed == 0) + { + g_udlist.push(x, y); + } +} + +/** + * @brief 计算Mandelbrot集的迭代次数 + * @param z Mandelbrot集的迭代状态 + * @return 迭代次数 + */ +int MandelbrotEx(state& z) +{ + if (z.iter >= 64) return z.iter; + int k = 64; + int b = k; + while (k > 0) + { + --k; + z.z = z.z * z.z + z.c; + if ( z.z.re*z.z.re + z.z.im*z.z.im > 4.0 ) + { + z.ed = 1; + break; + } + } + z.iter += b - k; + return z.iter; +} + +/** + * @brief 检查键盘和鼠标事件 + * @return 是否有键盘或鼠标事件发生 + */ +int kbmouhit() +{ + if (kbmsg()) return 1; + //return kbhit() || MouseHit(); + return 0; +} + +/** + * @brief 绘制Mandelbrot集 + * @param fromx X轴起始点 + * @param fromy Y轴起始点 + * @param tox X轴结束点 + * @param toy Y轴结束点 + * @return 更新的像素点数目 + */ +int MDraw(double fromx, double fromy, double tox, double toy) +{ + int t = clock(); + int ret = 0; +{ + int x, y; + while (g_udlist.pop(&x, &y)) + { + state& p = pMap[y][x]; + if (p.iter == 0 && p.ed == 0) + { + COMPLEX z, c; + c.re = fromx + (tox - fromx) * (x / (double)BF_W); + c.im = fromy + (toy - fromy) * (y / (double)BF_H); + z.re = z.im = 0.0; + p.c = c; + p.z = z; + } + if (p.ed == 0) + { + int k; + k = MandelbrotEx(p); + if (p.ed) + { + ret++; +{ + addpoint(x, y-1, k); + addpoint(x, y+1, k); + addpoint(x-1, y, k); + addpoint(x+1, y, k); +} + g_mi[y][x] = k; + /* + { + color_t c = 0; + c = colorMap(p.z, p.iter); + putpixel(x, y, c); + }// */ + } + else + { + addpoint(x, y); + } + } + if (kbmouhit()) break; + } +} + g_udlist.swap(); + return ret; +} + +int g_updatepoint = 0; + +///////////////////////////////////////////////// +// 绘制 Julia Set +///////////////////////////////////////////////// + +/** + * @brief 绘制Julia集 + * @param c 复数c + * @param fromx X轴起始点 + * @param fromy Y轴起始点 + * @param tox X轴结束点 + * @param toy Y轴结束点 + * @param sr sin(rotate) + * @param cr cos(rotate) + * @return 更新的像素点数目 + */ +int JDraw(COMPLEX c, double fromx, double fromy, double tox, double toy, double sr, double cr) +{ + int ret = 0; + int update = 0; + state* st = g_st - 1; + clock_t tt = clock(); + g_updatepoint = 0; + for(int y=0; yed) + { + continue; + } + COMPLEX& z = st->z; + + if (st->iter == 0) + { + double re = fromx + (tox - fromx) * (x / (double)g_w); + double im = fromy + (toy - fromy) * (y / (double)g_h); + z.re = cr * re + sr * im; + z.im = sr * re - cr * im; + } + else + { + //z = st->z; + } + st->iter++; +{ + z = z * z + c; + if ( z.re*z.re + z.im*z.im > bilout ) + { + st->ed = 1; + } +} + ++ret; + if ( st->ed ) + { + color_t c = 0; + c = colorMap(z, st->iter); + putpixel(x, y, c); + g_updatepoint += 1; + } + else if (st->iter == 1) + { + color_t c = 0; + //c = colorMap(z, st->iter); + putpixel_f(x, y, c); + } + } + if (clock() - tt > 10) + { + tt = clock(); + if (kbmouhit()) + { + return -1; + } + } + } + return ret; +} + +/** + * @brief 绘制Julia集(增量更新) + * @param c 复数c + * @param fromx X轴起始点 + * @param fromy Y轴起始点 + * @param tox X轴结束点 + * @param toy Y轴结束点 + * @return 更新的像素点数目 + */ +int JDrawA(COMPLEX c, double fromx, double fromy, double tox, double toy) +{ + clock_t tt = clock(); + int ret = 0; + g_updatepoint = 0; + state* st = g_st; +{ + int x, y; + while (g_udlist.pop(&x, &y)) + { + state& p = st[y * g_w + x]; + if (p.ed == 0) + { + int k; +{ + p.iter++; + k = p.iter; + COMPLEX &z = p.z; + z = z * z + c; + if ( z.re*z.re + z.im*z.im > bilout ) + { + p.ed = 1; + } +} + ret ++; + if (p.ed) + { + color_t c = 0; + c = colorMap(p.z, k); + putpixel(x, y, c); + g_updatepoint += 1; + } + else + { + g_udlist.push(x, y); + } + } + /* + if (clock() - tt > 10) + { + delay(1); + tt = clock(); + if (0 && kbmouhit()) + { + return -1; + } + }//*/ + } +} + g_udlist.swap(); + return ret; +} + +/** + * @brief 初始化状态数组 + * @param x X轴大小 + * @param y Y轴大小 */ +void init_st(int x, int y) +{ + memset(g_st, 0, x * y * sizeof(state)); +} + +/** + * @brief 主函数 + * @param argc 命令行参数个数 + * @param argv 命令行参数数组 + * @return 程序退出码 + */ +int main(int argc, char* argv[]) +{ + // 初始化绘图窗口及颜色 + setinitmode(0x005, 0, 0); + // if (argc < 2) + // { + // MessageBox(NULL, TEXT("本屏幕保护程序无配置"), TEXT("JuliaSet"), MB_OK); + // return 0; + // } + // else if (stricmp(argv[1], "/p") == 0) + // { + // HWND hwnd; + // sscanf(argv[2], "%d", &hwnd); + // attachHWND(hwnd); + // setinitmode(0x107, 0, 0); + // } + // else if (stricmp(argv[1], "/s")) + // { + // MessageBox(NULL, TEXT("本屏幕保护程序无配置"), TEXT("JuliaSet"), MB_OK); + // return 0; + // } + + //initgraph(320, 240); + initgraph(-1, -1); + + randomize(); + showmouse(0); + flushmouse(); + while(kbhit()) getch(); + + //InitColor(); + InitLog(); + g_w = getwidth(NULL); + g_h = getheight(NULL); + g_st = (state*)malloc(g_w * g_h * sizeof(state)); + COMPLEX c = {0.262, 0.002}, z = {0, 0}; + double r = 1.5, d = g_w / (double)g_h, rotate = 0, sr = sin(rotate), cr = cos(rotate); + init_st(g_w, g_h); + int n_update = 0; + double ftime = fclock(); +{ + double dc = 64, dca = 128, db = 16; + col_r = randomf() * dc + db; + col_g = randomf() * dc + db; + col_b = randomf() * dc + db; + col_ar = randomf() * dca; + col_ag = randomf() * dca; + col_ab = randomf() * dca; + rotate = randomf() * 360; + sr = sin(rotate), cr = cos(rotate); +} + setrendermode(RENDER_MANUAL); + for (int loop = 1; kbmouhit() == 0; ++loop) + { + int ret; + if (loop <= 4) + { + ret = JDraw(c, z.re - r * d, z.im - r, z.re + r * d, z.im + r, sr, cr); + if (loop == 4) + { + g_udlist.swap(); + for(int y=0; y 30) + { + delay(1); + t = clock(); + } + } + if (g_updatepoint == 0) + { + n_update++; + } + else + { + n_update = 0; + } + if (0) + { + char str[500]; + sprintf(str, "%d %d %f %f", g_w, g_h, r, d); + outtextxy(0, 0, str); + } + if (ret == 0 || n_update > 8 || loop > 1000) + { + loop = 0; + if (g_mi[0][0] == 0) + { + delay(1); + memset(pMap, 0, BF_W * BF_H * sizeof(state)); + g_udlist.clear(); + int i; + for (i = 0; i < BF_W; ++i) + { + addpoint(i, 0); + addpoint(i, BF_H - 1); + } + for (i = 0; i < 4; ) + { + if (MDraw(-1.9, -1.2, 0.5, 1.2) == 0) + { + ++i; + } + else + { + i = 0; + } + if (kbmouhit()) return 0; + } + } + double dc = 64, dca = 128, db = 16; + col_r = randomf() * dc + db; + col_g = randomf() * dc + db; + col_b = randomf() * dc + db; + col_ar = randomf() * dca; + col_ag = randomf() * dca; + col_ab = randomf() * dca; + rotate = randomf() * 360; + sr = sin(rotate), cr = cos(rotate); + do + { + c.re = randomf() * 2.4 - 1.9; + c.im = randomf() * 2.4 - 1.2; + int ir = (int)((c.re - (-1.9)) / (0.5 - (-1.9)) * BF_W); + int im = (int)((c.im - (-1.2)) / (1.2 - (-1.2)) * BF_H); + if (g_mi[im][ir] >= 16) + { + break; + } + } while (1); + init_st(g_w, g_h); + n_update = 0; + if (fclock() - ftime < 3) + { + delay_ms((int)((3 - (fclock() - ftime)) * 1000)); + } + else + { + delay(1); + } + ftime = fclock(); + } + } + + closegraph(); + return 0; +} + diff --git a/cmake_template/ege_demos/graph_kmeans.cpp b/cmake_template/ege_demos/graph_kmeans.cpp new file mode 100644 index 0000000..5aa0691 --- /dev/null +++ b/cmake_template/ege_demos/graph_kmeans.cpp @@ -0,0 +1,721 @@ +/** + * @file graph_kmeans.cpp + * @author wysaid (this@xege.org) + * @brief K-Means 聚类算法可视化演示 + * @version 0.1 + * @date 2025-11-27 + * + * K-Means 是一种经典的无监督学习聚类算法,用于将数据集划分为 K 个不同的簇。 + * 本程序通过动画演示 K-Means 算法的迭代过程,包括: + * 1. 初始化:随机选择 K 个点作为初始聚类中心 + * 2. 分配:将每个数据点分配到距离最近的聚类中心 + * 3. 更新:重新计算每个簇的中心点 + * 4. 迭代:重复分配和更新步骤直到收敛 + */ + +#ifndef NOMINMAX +#define NOMINMAX 1 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 文本本地化宏定义 +#ifdef _MSC_VER +// MSVC编译器使用中文文案 +#define TEXT_WINDOW_TITLE "K-Means 聚类算法演示" +#define TEXT_CONTROLS_TITLE "按键说明:" +#define TEXT_CONTROLS_START "S/空格/回车 - 开始/继续迭代" +#define TEXT_CONTROLS_RESET "R - 重置算法 (保留数据点)" +#define TEXT_CONTROLS_GENERATE "G - 重新生成数据点" +#define TEXT_CONTROLS_ADD_K "+/= - 增加簇数量 K" +#define TEXT_CONTROLS_SUB_K "-/_ - 减少簇数量 K" +#define TEXT_CONTROLS_AUTO "A - 自动演示模式" +#define TEXT_CONTROLS_SPEED "↑/↓ - 调整动画速度" +#define TEXT_CONTROLS_EXIT "ESC - 退出程序" +#define TEXT_CURRENT_K "当前簇数量 K = %d" +#define TEXT_ITERATION "迭代次数: %d" +#define TEXT_STATUS_READY "状态: 准备就绪" +#define TEXT_STATUS_RUNNING "状态: 迭代中..." +#define TEXT_STATUS_CONVERGED "状态: 已收敛!" +#define TEXT_STATUS_AUTO "状态: 自动演示中..." +#define TEXT_POINTS_COUNT "数据点数量: %d" +#define TEXT_ANIMATION_SPEED "动画速度: %d ms" +#define TEXT_FONT_NAME "宋体" +#define TEXT_CLUSTER_INFO "簇 %d: %d 个点" +#define TEXT_CENTROID_MOVED "中心点移动距离: %.2f" +#else +// 非MSVC编译器使用英文文案 +#define TEXT_WINDOW_TITLE "K-Means Visualization" +#define TEXT_CONTROLS_TITLE "Controls:" +#define TEXT_CONTROLS_START "S/Space/Enter - Start/Continue Iteration" +#define TEXT_CONTROLS_RESET "R - Reset Algorithm (Keep Data)" +#define TEXT_CONTROLS_GENERATE "G - Generate New Data Points" +#define TEXT_CONTROLS_ADD_K "+/= - Increase K" +#define TEXT_CONTROLS_SUB_K "-/_ - Decrease K" +#define TEXT_CONTROLS_AUTO "A - Auto Demo Mode" +#define TEXT_CONTROLS_SPEED "Up/Down - Adjust Animation Speed" +#define TEXT_CONTROLS_EXIT "ESC - Exit Program" +#define TEXT_CURRENT_K "Current K = %d" +#define TEXT_ITERATION "Iterations: %d" +#define TEXT_STATUS_READY "Status: Ready" +#define TEXT_STATUS_RUNNING "Status: Running..." +#define TEXT_STATUS_CONVERGED "Status: Converged!" +#define TEXT_STATUS_AUTO "Status: Auto Demo..." +#define TEXT_POINTS_COUNT "Data Points: %d" +#define TEXT_ANIMATION_SPEED "Animation Speed: %d ms" +#define TEXT_FONT_NAME "Arial" +#define TEXT_CLUSTER_INFO "Cluster %d: %d points" +#define TEXT_CENTROID_MOVED "Centroid moved: %.2f" +#endif + +// 窗口和可视化参数 +const int WINDOW_WIDTH = 1200; +const int WINDOW_HEIGHT = 800; +const int PANEL_WIDTH = 280; // 右侧控制面板宽度 +const int CANVAS_WIDTH = WINDOW_WIDTH - PANEL_WIDTH; // 绘图区域宽度 +const int CANVAS_HEIGHT = WINDOW_HEIGHT; // 绘图区域高度 +const int POINT_RADIUS = 5; // 数据点半径 +const int CENTROID_RADIUS = 12; // 聚类中心半径 +const int DEFAULT_K = 5; // 默认簇数量 +const int MIN_K = 2; // 最小簇数量 +const int MAX_K = 10; // 最大簇数量 +const int DEFAULT_POINTS = 300; // 默认数据点数量 +const int MIN_POINTS = 50; // 最小数据点数量 +const int MAX_POINTS = 1000; // 最大数据点数量 + +// 预定义的簇颜色 (鲜艳的颜色便于区分) +const color_t CLUSTER_COLORS[] = { + EGERGB(231, 76, 60), // 红色 + EGERGB(46, 204, 113), // 绿色 + EGERGB(52, 152, 219), // 蓝色 + EGERGB(241, 196, 15), // 黄色 + EGERGB(155, 89, 182), // 紫色 + EGERGB(230, 126, 34), // 橙色 + EGERGB(26, 188, 156), // 青色 + EGERGB(236, 240, 241), // 白色 + EGERGB(241, 148, 138), // 粉色 + EGERGB(133, 193, 233), // 浅蓝 +}; + +/** + * @struct Point2D + * @brief 2D 数据点结构 + */ +struct Point2D +{ + float x; // x 坐标 + float y; // y 坐标 + int clusterId; // 所属簇的 ID (-1 表示未分配) + + Point2D(float _x = 0, float _y = 0) : x(_x), y(_y), clusterId(-1) {} + + // 计算到另一个点的欧几里得距离 + float distanceTo(const Point2D& other) const + { + float dx = x - other.x; + float dy = y - other.y; + return std::sqrt(dx * dx + dy * dy); + } +}; + +/** + * @struct Centroid + * @brief 聚类中心结构 + */ +struct Centroid +{ + float x; // x 坐标 + float y; // y 坐标 + float prevX; // 上一次迭代的 x 坐标 (用于绘制移动轨迹) + float prevY; // 上一次迭代的 y 坐标 + + Centroid(float _x = 0, float _y = 0) : x(_x), y(_y), prevX(_x), prevY(_y) {} + + // 计算移动距离 + float movedDistance() const + { + float dx = x - prevX; + float dy = y - prevY; + return std::sqrt(dx * dx + dy * dy); + } + + // 保存当前位置 + void savePosition() + { + prevX = x; + prevY = y; + } +}; + +/** + * @enum AlgorithmState + * @brief 算法状态枚举 + */ +enum AlgorithmState +{ + STATE_READY, // 准备就绪 + STATE_RUNNING, // 运行中 + STATE_CONVERGED, // 已收敛 + STATE_AUTO // 自动演示模式 +}; + +/** + * @class KMeansVisualizer + * @brief K-Means 算法可视化类 + */ +class KMeansVisualizer +{ +public: + KMeansVisualizer() + : m_k(DEFAULT_K) + , m_numPoints(DEFAULT_POINTS) + , m_iteration(0) + , m_state(STATE_READY) + , m_animationSpeed(300) + , m_convergenceThreshold(0.5f) + , m_autoMode(false) + { + m_rd.seed(static_cast(std::time(nullptr))); + generateDataPoints(); + initializeCentroids(); + } + + // 生成随机数据点 (使用高斯分布生成多个簇状分布) + void generateDataPoints() + { + m_points.clear(); + + // 生成几个随机簇中心 + int numClusters = m_rd() % 4 + 3; // 3-6 个自然簇 + float clusterSpread = 60.0f; // 簇内点的分散程度 + + std::vector clusterCenters; + for (int i = 0; i < numClusters; ++i) { + float cx = static_cast(m_rd() % (CANVAS_WIDTH - 100) + 50); + float cy = static_cast(m_rd() % (CANVAS_HEIGHT - 100) + 50); + clusterCenters.push_back(Point2D(cx, cy)); + } + + // 围绕每个簇中心生成点 + std::normal_distribution normalDist(0.0f, clusterSpread); + int pointsPerCluster = m_numPoints / numClusters; + + for (int i = 0; i < numClusters; ++i) { + int count = (i == numClusters - 1) ? (m_numPoints - static_cast(m_points.size())) : pointsPerCluster; + for (int j = 0; j < count; ++j) { + float x = clusterCenters[i].x + normalDist(m_rd); + float y = clusterCenters[i].y + normalDist(m_rd); + + // 确保点在画布范围内 + x = std::max(10.0f, std::min(static_cast(CANVAS_WIDTH - 10), x)); + y = std::max(10.0f, std::min(static_cast(CANVAS_HEIGHT - 10), y)); + + m_points.push_back(Point2D(x, y)); + } + } + + // 添加一些随机噪声点 + int noiseCount = m_numPoints / 10; + for (int i = 0; i < noiseCount; ++i) { + float x = static_cast(m_rd() % (CANVAS_WIDTH - 20) + 10); + float y = static_cast(m_rd() % (CANVAS_HEIGHT - 20) + 10); + m_points.push_back(Point2D(x, y)); + } + + m_numPoints = static_cast(m_points.size()); + } + + // 初始化聚类中心 (使用 K-Means++ 初始化策略) + void initializeCentroids() + { + m_centroids.clear(); + m_iteration = 0; + m_state = STATE_READY; + + if (m_points.empty() || m_k <= 0) return; + + // K-Means++ 初始化 + // 1. 随机选择第一个中心点 + int firstIdx = m_rd() % m_points.size(); + m_centroids.push_back(Centroid(m_points[firstIdx].x, m_points[firstIdx].y)); + + // 2. 依次选择其余中心点,选择概率与到最近中心点的距离平方成正比 + std::vector distances(m_points.size()); + + for (int c = 1; c < m_k; ++c) { + float totalDist = 0.0f; + + // 计算每个点到最近中心点的距离 + for (size_t i = 0; i < m_points.size(); ++i) { + float minDist = std::numeric_limits::max(); + for (const auto& centroid : m_centroids) { + float dx = m_points[i].x - centroid.x; + float dy = m_points[i].y - centroid.y; + float dist = dx * dx + dy * dy; + minDist = std::min(minDist, dist); + } + distances[i] = minDist; + totalDist += minDist; + } + + // 按概率选择下一个中心点 + float r = static_cast(m_rd()) / static_cast(m_rd.max()) * totalDist; + float cumulative = 0.0f; + int selectedIdx = 0; + + for (size_t i = 0; i < m_points.size(); ++i) { + cumulative += distances[i]; + if (cumulative >= r) { + selectedIdx = static_cast(i); + break; + } + } + + m_centroids.push_back(Centroid(m_points[selectedIdx].x, m_points[selectedIdx].y)); + } + + // 重置所有点的簇分配 + for (auto& point : m_points) { + point.clusterId = -1; + } + } + + // 执行一次 K-Means 迭代 + bool iterate() + { + if (m_centroids.empty() || m_points.empty()) return true; + + // 保存当前中心点位置 + for (auto& centroid : m_centroids) { + centroid.savePosition(); + } + + // 步骤1:分配每个点到最近的中心点 + for (auto& point : m_points) { + float minDist = std::numeric_limits::max(); + int bestCluster = 0; + + for (int k = 0; k < m_k; ++k) { + float dx = point.x - m_centroids[k].x; + float dy = point.y - m_centroids[k].y; + float dist = dx * dx + dy * dy; + + if (dist < minDist) { + minDist = dist; + bestCluster = k; + } + } + point.clusterId = bestCluster; + } + + // 步骤2:更新中心点位置 + std::vector sumX(m_k, 0.0f); + std::vector sumY(m_k, 0.0f); + std::vector counts(m_k, 0); + + for (const auto& point : m_points) { + if (point.clusterId >= 0 && point.clusterId < m_k) { + sumX[point.clusterId] += point.x; + sumY[point.clusterId] += point.y; + counts[point.clusterId]++; + } + } + + for (int k = 0; k < m_k; ++k) { + if (counts[k] > 0) { + m_centroids[k].x = sumX[k] / counts[k]; + m_centroids[k].y = sumY[k] / counts[k]; + } + } + + ++m_iteration; + + // 检查是否收敛 + float maxMoved = 0.0f; + for (const auto& centroid : m_centroids) { + maxMoved = std::max(maxMoved, centroid.movedDistance()); + } + + return maxMoved < m_convergenceThreshold; + } + + // 绘制所有内容 + void draw() + { + // 清空画布 + setbkcolor(EGERGB(30, 30, 40)); + cleardevice(); + + ege_enable_aa(true); + + // 绘制数据点 + drawPoints(); + + // 绘制中心点移动轨迹 + drawCentroidTrails(); + + // 绘制聚类中心 + drawCentroids(); + + // 绘制控制面板 + drawControlPanel(); + } + + // 绘制数据点 + void drawPoints() + { + for (const auto& point : m_points) { + color_t color; + if (point.clusterId >= 0 && point.clusterId < m_k) { + color = CLUSTER_COLORS[point.clusterId % (sizeof(CLUSTER_COLORS) / sizeof(CLUSTER_COLORS[0]))]; + } else { + color = EGERGB(128, 128, 128); // 未分配的点为灰色 + } + + setfillcolor(color); + setcolor(EGEACOLOR(200, color)); + ege_fillellipse(point.x - POINT_RADIUS, point.y - POINT_RADIUS, POINT_RADIUS * 2, POINT_RADIUS * 2); + } + } + + // 绘制中心点移动轨迹 + void drawCentroidTrails() + { + setlinestyle(PS_DASH, 2); + for (int k = 0; k < static_cast(m_centroids.size()); ++k) { + const auto& centroid = m_centroids[k]; + if (centroid.movedDistance() > 0.1f) { + color_t color = CLUSTER_COLORS[k % (sizeof(CLUSTER_COLORS) / sizeof(CLUSTER_COLORS[0]))]; + setcolor(EGEACOLOR(150, color)); + line(static_cast(centroid.prevX), static_cast(centroid.prevY), + static_cast(centroid.x), static_cast(centroid.y)); + } + } + setlinestyle(PS_SOLID, 1); + } + + // 绘制聚类中心 + void drawCentroids() + { + for (int k = 0; k < static_cast(m_centroids.size()); ++k) { + const auto& centroid = m_centroids[k]; + color_t color = CLUSTER_COLORS[k % (sizeof(CLUSTER_COLORS) / sizeof(CLUSTER_COLORS[0]))]; + + // 绘制十字星标记 + setcolor(ege::WHITE); + setlinestyle(PS_SOLID, 3); + + int cx = static_cast(centroid.x); + int cy = static_cast(centroid.y); + + // 外圈 + setfillcolor(color); + ege_fillellipse(cx - CENTROID_RADIUS, cy - CENTROID_RADIUS, CENTROID_RADIUS * 2, CENTROID_RADIUS * 2); + + // 内圈 (白色) + setfillcolor(ege::WHITE); + ege_fillellipse(cx - CENTROID_RADIUS / 2, cy - CENTROID_RADIUS / 2, CENTROID_RADIUS, CENTROID_RADIUS); + + // 绘制十字 + setcolor(color); + setlinestyle(PS_SOLID, 2); + line(cx - CENTROID_RADIUS - 5, cy, cx + CENTROID_RADIUS + 5, cy); + line(cx, cy - CENTROID_RADIUS - 5, cx, cy + CENTROID_RADIUS + 5); + } + } + + // 绘制控制面板 + void drawControlPanel() + { + int panelX = CANVAS_WIDTH; + + // 面板背景 + setfillcolor(EGERGB(45, 45, 55)); + bar(panelX, 0, WINDOW_WIDTH, WINDOW_HEIGHT); + + // 分隔线 + setcolor(EGERGB(80, 80, 90)); + line(panelX, 0, panelX, WINDOW_HEIGHT); + + // 设置字体 + setfont(18, 0, TEXT_FONT_NAME); + setcolor(ege::WHITE); + + int textX = panelX + 15; + int textY = 20; + int lineHeight = 28; + + // 标题 + setfont(22, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_WINDOW_TITLE); + textY += lineHeight + 10; + + // 分隔线 + setcolor(EGERGB(80, 80, 90)); + line(panelX + 10, textY, WINDOW_WIDTH - 10, textY); + textY += 15; + + // 状态信息 + setfont(16, 0, TEXT_FONT_NAME); + setcolor(EGERGB(150, 200, 255)); + + char buf[128]; + + // 显示 K 值 + sprintf(buf, TEXT_CURRENT_K, m_k); + outtextxy(textX, textY, buf); + textY += lineHeight; + + // 显示迭代次数 + sprintf(buf, TEXT_ITERATION, m_iteration); + outtextxy(textX, textY, buf); + textY += lineHeight; + + // 显示数据点数量 + sprintf(buf, TEXT_POINTS_COUNT, m_numPoints); + outtextxy(textX, textY, buf); + textY += lineHeight; + + // 显示动画速度 + sprintf(buf, TEXT_ANIMATION_SPEED, m_animationSpeed); + outtextxy(textX, textY, buf); + textY += lineHeight + 5; + + // 显示状态 + setcolor(EGERGB(100, 255, 100)); + switch (m_state) { + case STATE_READY: + outtextxy(textX, textY, TEXT_STATUS_READY); + break; + case STATE_RUNNING: + setcolor(EGERGB(255, 200, 100)); + outtextxy(textX, textY, TEXT_STATUS_RUNNING); + break; + case STATE_CONVERGED: + setcolor(EGERGB(100, 255, 200)); + outtextxy(textX, textY, TEXT_STATUS_CONVERGED); + break; + case STATE_AUTO: + setcolor(EGERGB(255, 150, 200)); + outtextxy(textX, textY, TEXT_STATUS_AUTO); + break; + } + textY += lineHeight + 10; + + // 分隔线 + setcolor(EGERGB(80, 80, 90)); + line(panelX + 10, textY, WINDOW_WIDTH - 10, textY); + textY += 15; + + // 显示各簇信息 + setfont(14, 0, TEXT_FONT_NAME); + std::vector clusterCounts(m_k, 0); + for (const auto& point : m_points) { + if (point.clusterId >= 0 && point.clusterId < m_k) { + clusterCounts[point.clusterId]++; + } + } + + for (int k = 0; k < m_k; ++k) { + color_t color = CLUSTER_COLORS[k % (sizeof(CLUSTER_COLORS) / sizeof(CLUSTER_COLORS[0]))]; + setfillcolor(color); + bar(textX, textY + 2, textX + 12, textY + 14); + setcolor(ege::WHITE); + sprintf(buf, TEXT_CLUSTER_INFO, k + 1, clusterCounts[k]); + outtextxy(textX + 18, textY, buf); + textY += 22; + } + + textY += 10; + + // 分隔线 + setcolor(EGERGB(80, 80, 90)); + line(panelX + 10, textY, WINDOW_WIDTH - 10, textY); + textY += 15; + + // 控制说明 + setcolor(EGERGB(200, 200, 200)); + setfont(14, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_CONTROLS_TITLE); + textY += lineHeight; + + setcolor(EGERGB(180, 180, 180)); + setfont(12, 0, TEXT_FONT_NAME); + outtextxy(textX, textY, TEXT_CONTROLS_START); + textY += 20; + outtextxy(textX, textY, TEXT_CONTROLS_RESET); + textY += 20; + outtextxy(textX, textY, TEXT_CONTROLS_GENERATE); + textY += 20; + outtextxy(textX, textY, TEXT_CONTROLS_ADD_K); + textY += 20; + outtextxy(textX, textY, TEXT_CONTROLS_SUB_K); + textY += 20; + outtextxy(textX, textY, TEXT_CONTROLS_AUTO); + textY += 20; + outtextxy(textX, textY, TEXT_CONTROLS_SPEED); + textY += 20; + outtextxy(textX, textY, TEXT_CONTROLS_EXIT); + } + + // 处理键盘输入 + void handleInput() + { + while (kbhit()) { + int key = getch(); + + switch (key) { + case 'S': + case 's': + case ' ': + case '\r': + case '\n': + // 执行一次迭代或开始 + if (m_state != STATE_CONVERGED) { + m_state = STATE_RUNNING; + if (iterate()) { + m_state = STATE_CONVERGED; + } + } + break; + + case 'R': + case 'r': + // 重置算法 + initializeCentroids(); + m_autoMode = false; + break; + + case 'G': + case 'g': + // 重新生成数据点 + generateDataPoints(); + initializeCentroids(); + m_autoMode = false; + break; + + case 'A': + case 'a': + // 切换自动模式 + m_autoMode = !m_autoMode; + if (m_autoMode) { + m_state = STATE_AUTO; + } else { + if (m_state == STATE_AUTO) { + m_state = STATE_READY; + } + } + break; + + case '+': + case '=': + // 增加 K + if (m_k < MAX_K) { + ++m_k; + initializeCentroids(); + } + break; + + case '-': + case '_': + // 减少 K + if (m_k > MIN_K) { + --m_k; + initializeCentroids(); + } + break; + + case key_up: + // 加快动画速度 + m_animationSpeed = std::max(50, m_animationSpeed - 50); + break; + + case key_down: + // 减慢动画速度 + m_animationSpeed = std::min(1000, m_animationSpeed + 50); + break; + + case key_esc: + // 退出 + closegraph(); + exit(0); + break; + } + } + } + + // 自动演示模式更新 + void autoUpdate() + { + if (m_autoMode && m_state != STATE_CONVERGED) { + m_state = STATE_AUTO; + if (iterate()) { + m_state = STATE_CONVERGED; + m_autoMode = false; + } + } + } + + // 获取动画速度 + int getAnimationSpeed() const { return m_animationSpeed; } + + // 是否在自动模式 + bool isAutoMode() const { return m_autoMode; } + +private: + std::vector m_points; // 数据点 + std::vector m_centroids; // 聚类中心 + int m_k; // 簇数量 + int m_numPoints; // 数据点数量 + int m_iteration; // 迭代次数 + AlgorithmState m_state; // 算法状态 + int m_animationSpeed; // 动画速度 (毫秒) + float m_convergenceThreshold;// 收敛阈值 + bool m_autoMode; // 自动演示模式 + std::mt19937 m_rd; // 随机数生成器 +}; + +/** + * @brief 程序入口 + */ +int main() +{ + // 初始化图形窗口 + setinitmode(INIT_ANIMATION); + initgraph(WINDOW_WIDTH, WINDOW_HEIGHT); + setcaption(TEXT_WINDOW_TITLE); + setbkmode(TRANSPARENT); + + // 创建可视化器 + KMeansVisualizer visualizer; + + // 主循环 + for (; is_run(); delay_fps(60)) { + // 处理输入 + visualizer.handleInput(); + + // 自动模式更新 + if (visualizer.isAutoMode()) { + static int frameCount = 0; + int framesPerStep = visualizer.getAnimationSpeed() / 16; // 约 60fps + if (++frameCount >= framesPerStep) { + visualizer.autoUpdate(); + frameCount = 0; + } + } + + // 绘制 + visualizer.draw(); + } + + closegraph(); + return 0; +} diff --git a/cmake_template/ege_demos/graph_lines.cpp b/cmake_template/ege_demos/graph_lines.cpp new file mode 100644 index 0000000..16dd302 --- /dev/null +++ b/cmake_template/ege_demos/graph_lines.cpp @@ -0,0 +1,258 @@ +/********************************************************************** + * 文件名:egelines.cpp + * + * 程序目的:实现变幻线屏保效果 + * + * 使用的图形库:EGE + * + **********************************************************************/ + +#include +#include +#include +#include +#include "ege/fps.h" + +int width = 640, height = 480; + +/** + * @brief 点结构体,包含点的坐标和速度 + */ +struct point +{ + double x; /**< 点的横坐标 */ + double y; /**< 点的纵坐标 */ + double dx; /**< 点在横向上的速度 */ + double dy; /**< 点在纵向上的速度 */ +}; + +/** + * @brief 多边形结构体,包含点的个数和点的数组 + */ +struct poly +{ + int n_point; /**< 点的个数 */ + point p[20]; /**< 点的数组 */ +}; + +/** + * @brief 多边形队列组结构体 + */ +struct polys +{ + int n_poly; /**< 多边形队列的长度 */ + int color; /**< 当前颜色 */ + int nextcolor, prevcolor; /**< 上一次的颜色和目标颜色 */ + int chtime, nowtime; /**< 过渡变化时间和当前时间 */ + int time; /**< 距离下次改变颜色的时间 */ + poly p[100]; /**< 多边形数组 */ +}; + +/** + * @brief 返回一个位于 [db, db+dv] 范围内的随机浮点数 + * @param dv 随机数范围的大小 + * @param db 随机数范围的起始点 + * @return 随机生成的浮点数 + */ +double rand_float(double dv, double db) +{ + return randomf() * dv + db; +} + +/** + * @brief 根据点的速度属性移动点的位置,若移出屏幕则进行反弹计算 + * @param b 需要移动的点 + */ +void movepoint(struct point* b) +{ + double dv = 1.0, db = 0.5; + double tw = width / 640.0, th = height / 480.0; + + if (b->x < 0) + b->dx = rand_float(dv, db) * tw; + if (b->y < 0) + b->dy = rand_float(dv, db) * th; + if (b->x > width) + b->dx = -rand_float(dv, db) * tw; + if (b->y > height) + b->dy = -rand_float(dv, db) * th; + + b->x += b->dx; + b->y += b->dy; +} + +/** + * @brief 移动单个多边形,内部调用点的移动 + * @param p 需要移动的多边形 + */ +void movepoly(struct poly* p) +{ + int i; + + for (i = 0; i < p->n_point; ++i) + { + movepoint(&(p->p[i])); + } +} + +/** + * @brief 移动多边形队列,包含时间检测和颜色计算 + * @param p 需要移动的多边形队列 + */ +void movepolys(struct polys* p) +{ + int i; + + for (i = p->n_poly - 1; i > 0; --i) + { + p->p[i] = p->p[i - 1]; + } + + movepoly(p->p); + ++(p->nowtime); + + if (--(p->time) <= 0) + { + p->prevcolor = p->color; + p->nextcolor = HSVtoRGB((float)random(360), 1.0f, (float)rand_float(0.5, 0.5)); + p->time = random(1000); + p->chtime = random(1000) + 60; + p->nowtime = 0; + } + + if (p->nowtime >= p->chtime) + { + p->color = p->nextcolor; + } + else + { + double dr = p->prevcolor & 0xFF, dg = (p->prevcolor >> 8) & 0xFF, db = (p->prevcolor >> 16) & 0xFF; + double dt = 1 - p->nowtime / (double)(p->chtime); + + dr -= p->nextcolor & 0xFF, dg -= (p->nextcolor >> 8) & 0xFF, db -= (p->nextcolor >> 16) & 0xFF; + dr *= dt, dg *= dt, db *= dt; + dr += p->nextcolor & 0xFF, dg += (p->nextcolor >> 8) & 0xFF, db += (p->nextcolor >> 16) & 0xFF; + + p->color = ((int)dr) | ((int)dg << 8) | ((int)db << 16); + } +} + +/** + * @brief 初始化多边形队列组 + * @param p 多边形队列组 + * @param npoly 多边形队列的长度 + * @param npoint 多边形中点的个数 + */ +void initpolys(struct polys* p, int npoly, int npoint) +{ + int i, j; + + p->n_poly = npoly; + p->color = 0; + p->time = 1000; + p->prevcolor = p->color; + p->nextcolor = HSVtoRGB((float)random(360), 1.0f, 0.5f); + p->chtime = 1000; + p->nowtime = 0; + + j = 0; + p->p[j].n_point = npoint; + + for (i = 0; i < npoint; ++i) + { + p->p[j].p[i].x = random(width); + p->p[j].p[i].y = random(height); + p->p[j].p[i].dx = (randomf() * 2 + 1); + p->p[j].p[i].dy = (randomf() * 2 + 1); + } + + for (j = 1; j < npoly; ++j) + { + p->p[i] = p->p[i - 1]; + } +} + +/** + * @brief + + 绘制一个多边形 + * @param p 多边形对象 + * @param color 颜色值 + */ +void draw_poly(struct poly* p, int color) +{ + int points[100]; + int i; + + for (i = 0; i < p->n_point; ++i) + { + points[i * 2] = (int)(p->p[i].x + 0.5f); + points[i * 2 + 1] = (int)(p->p[i].y + 0.5f); + } + + points[i * 2] = (int)(p->p[0].x + 0.5f); + points[i * 2 + 1] = (int)(p->p[0].y + 0.5f); + + setcolor(color); + drawpoly(p->n_point + 1, points); +} + +/** + * @brief 绘制多边形队列的多边形(只绘制第一个和最后一个,最后一个用于擦除) + * @param p 多边形队列对象 + */ +void draw_polys(struct polys* p) +{ + draw_poly(&(p->p[p->n_poly - 1]), 0); + draw_poly(&(p->p[0]), p->color); +} + +int main() +{ + static struct polys p[10] = {{0}}; + int n_points[10] = {4, 3, 5, 6, 7}; + int n_poly[10] = {80, 40, 10, 5, 1}; + int n_polys = 2, i; + randomize(); + + // 图形初始化 +{ + setinitmode(1, 0, 0); + initgraph(-1, -1); + width = getmaxx(); + height = getmaxy(); + setrendermode(RENDER_MANUAL); +} + + // 多边形对象初始化 + for (i = 0; i < n_polys; ++i) + { + initpolys(&p[i], n_poly[i], n_points[i]); + } + + setfont(12, 6, "宋体"); + fps ui_fps; + + // 主循环 + for (; is_run(); delay_fps(60)) + { + if (kbhit() > 0) // 有按键按下就退出 + { + break; + } + + for (i = 0; i < n_polys; ++i) + { + movepolys(&(p[i])); + } + + for (i = 0; i < n_polys; ++i) + { + draw_polys(&(p[i])); + } + } + + closegraph(); + return 0; +} + diff --git a/cmake_template/ege_demos/graph_mandelbrot.cpp b/cmake_template/ege_demos/graph_mandelbrot.cpp new file mode 100644 index 0000000..5468ebd --- /dev/null +++ b/cmake_template/ege_demos/graph_mandelbrot.cpp @@ -0,0 +1,222 @@ +//鼠标放大mandelbrot集演示 +#include + +// 定义常量 +#define ITERATIONS 1000 // 迭代次数,越高,图像越精细 +#define MAXCOLOR 300 // 颜色数,越多图像越平滑,但不大于迭代次数 + + +///////////////////////////////////////////////// +// 定义复数及乘、加运算 +///////////////////////////////////////////////// + +// 定义复数 +struct COMPLEX +{ + double re; + double im; +}; + +// 定义复数“乘”运算 +COMPLEX mul(COMPLEX a, COMPLEX b) +{ + COMPLEX c; + c.re = a.re * b.re - a.im * b.im; + c.im = a.im * b.re + a.re * b.im; + return c; +} + +// 定义复数“加”运算 +COMPLEX add(COMPLEX a, COMPLEX b) +{ + COMPLEX c; + c.re = a.re + b.re; + c.im = a.im + b.im; + return c; +} + + +///////////////////////////////////////////////// +// 定义颜色及初始化颜色 +///////////////////////////////////////////////// + +// 定义颜色 +int Color[MAXCOLOR]; + +// 初始化颜色 +void InitColor() +{ + // 使用 HSL 颜色模式产生角度 h1 到 h2 的渐变色 + int h1 = 240, h2 = 330, i; + for (i=0; i 4.0 ) + { + break; //其模超过4,肯定发散,跳出 + } + } + return maxcalc; +} + + +///////////////////////////////////////////////// +// 绘制 Mandelbrot Set (曼德布洛特集) +///////////////////////////////////////////////// +void Draw(double fromx, double fromy, double tox, double toy) +{ + COMPLEX z, c; + int x, y; + for (x=0; x<640; x++) + { + c.re = fromx + (tox - fromx) * (x / 640.0); + for (y=0; y<480; y++) + { + int k; + c.im = fromy + (toy - fromy) * (y / 480.0); + k = f(c); + if (k > 0) k = Color[(ITERATIONS - k) % MAXCOLOR]; + putpixel(x, y, k); + } + } +} + + +///////////////////////////////////////////////// +// 主函数 +///////////////////////////////////////////////// +int main() +{ + double fromx, fromy, tox, toy; + + // 初始化绘图窗口及颜色 + initgraph(640, 480); + InitColor(); + + + // 初始化 Mandelbrot Set(曼德布洛特集)坐标系 + fromx = -2.2; tox = 2.2; + fromy = -1.65; toy = 1.65; + Draw(fromx, fromy, tox, toy); + + + // 捕获鼠标操作,实现放大鼠标选中区域 + { + mouse_msg m; + bool isLDown = false; + int selfx, selfy, seltx, selty; // 定义选区 + + while (is_run()) + { + m = getmouse(); // 获取一条鼠标消息 + + switch (m.msg) + { + case mouse_msg_up: + if (m.is_right()) { + // 按鼠标右键恢复原图形坐标系 + fromx = -2.2; tox = 1.2; + fromy = -1.65; toy = 1.65; + Draw(fromx, fromy, tox, toy); + } else { + // 按鼠标左键并拖动,选择区域 + rectangle(selfx, selfy, seltx, selty); + setwritemode(R2_COPYPEN); + isLDown = false; + seltx = m.x; + selty = m.y; + + if (selfx == seltx || selfy == selty) break; + + // 修正选区为 4:3 + { + int tmp; + if (selfx > seltx) + { + tmp = selfx; selfx = seltx; seltx = tmp; + } + if (selfy > selty) + { + tmp = selfy; selfy = selty; selty = tmp; + } + } + + if ( (seltx - selfx) * 0.75 < (selty - selfy) ) + { + selty += (3 - (selty - selfy) % 3); + selfx -= (selty - selfy) / 3 * 4 / 2 + - (seltx - selfx) / 2; + seltx = selfx + (selty - selfy) / 3 * 4; + } + else + { + seltx += (4 - (seltx - selfx) % 4); + selfy -= (seltx - selfx) * 3 / 4 / 2 + - (selty - selfy ) / 2; + selty = selfy + (seltx - selfx ) * 3 / 4; + } + + // 更新坐标系 + { + double f, t; + f = fromx + (tox - fromx) * selfx / 640; + t = fromx + (tox - fromx) * seltx / 640; + fromx = f; + tox = t; + f = fromy + (toy - fromy) * selfy / 480; + t = fromy + (toy - fromy) * selty / 480; + fromy = f; + toy = t; + } + + // 画图形 + Draw(fromx, fromy, tox, toy); + } + break; + + case mouse_msg_move: + // 按鼠标左键并拖动,选择区域 + if (isLDown) + { + rectangle(selfx, selfy, seltx, selty); + seltx = m.x; + selty = m.y; + rectangle(selfx, selfy, seltx, selty); + } + break; + + case mouse_msg_down: + // 按鼠标左键并拖动,选择区域 + if (m.is_left()) + { + setcolor(WHITE); + setwritemode(R2_XORPEN); + isLDown = true; + selfx = seltx = m.x; + selfy = selty = m.y; + rectangle(selfx, selfy, seltx, selty); + } + + break; + } + } + } + + getch(); + closegraph(); + return 0; +} + diff --git a/cmake_template/ege_demos/graph_mouseball.cpp b/cmake_template/ege_demos/graph_mouseball.cpp new file mode 100644 index 0000000..814c196 --- /dev/null +++ b/cmake_template/ege_demos/graph_mouseball.cpp @@ -0,0 +1,207 @@ +// 鼠标拖动弹球演示 +#include +#include + +#define LEN 640 +#define WID 480 +#define MAXBALL 10 + +class BALL +{ + public: + BALL() + { + r = random(20) + 20; + x = random(LEN - r * 2) + r; + y = random(WID - r * 2) + r; + vx = (6 * randomf() + 0.1f) * (random(2) * 2.0 - 1); + vy = (6 * randomf() + 0.1f) * (random(2) * 2.0 - 1); + color = HSVtoRGB(randomf() * 360.0f, 1.0f, 0.8f); + + float a = randomf();//随机下每个对象的摩擦系数 + + fa = 1 / (100.0); + goon = 1; + } + + void drawball() + { + setfillcolor(color); + setcolor(color); + fillellipse((int)x, (int)y, (int)r, (int)r); + } + + void update() + { + + if(x - r <= 0) + { + x = r; + if ( vx <= 0) + vx = -vx; + } + if (x + r >= LEN) + { + x = LEN - r; + if (vx >= 0) + vx = -vx; + } + if(y - r <= 0) + { + y = r; + if (vy <= 0) + vy = -vy; + } + if (y + r >= WID) + { + y = WID - r; + if (vy >= 0) + vy = -vy; + } + if (goon) + { + x += vx; + y += vy; + + double fv = sqrt((double)vx * vx + (double)vy * vy); + if (fv > 1e-9) + { + vx = vx - vx * fa / fv; + vy = vy - vy * fa / fv; + } + else + { + vx = 0; + vy = 0; + } + } + } + bool isCrash(int _x, int _y) + { + double dx = _x - x, dy = _y - y; + return dx * dx + dy * dy < r * r; + } + + bool resmouse(mouse_msg mouse, double dx, double dy) + { + double f = 0.9; + if(mouse.msg == mouse_msg_down) + { + if( isCrash(mouse.x, mouse.y) )//如果鼠标的位置在圆内。。 + { + vx = 0; + vy = 0; + x = mouse.x; + y = mouse.y; + goon = 0; + return 1; + } + } + else if (mouse.msg == mouse_msg_up) + { + goon = 1; + return 0; + } + else if (mouse.msg == mouse_msg_move) + { + if (dx*dx + dy*dy > vx*vx + vy*vy) + { + vx = dx; + vy = dy; + } + else + { + vx *= f; + vy *= f; + } + x = mouse.x; + y = mouse.y; + goon = 0; + return 1; + } + else if (! goon) + { + vx *= f; + vy *= f; + } + return 0; + } + + + private: + int r; + float x, y; + float vx, vy;//速度分量 + int color; + float ax, ay;//加速度分量 + float fa;//摩擦 + bool goon;//标志 +}; + +void dealMouse(BALL* ball) +{ + static int iCapture = -1, mx, my; + int i; + mouse_msg mouse = {0}; + while (mousemsg()) + { + double dx, dy; + mouse = getmouse(); + dx = (mouse.x - mx) * 1.0f; + dy = (mouse.y - my) * 1.0f; + mx = mouse.x; + my = mouse.y; + + if(iCapture == -1 && mouse.msg == mouse_msg_down) + { + for(i = MAXBALL - 1; i > -1; --i) + if(ball[i].resmouse(mouse, dx, dy)) + { + iCapture = i; + break; + } + } + else if (iCapture >= 0 && (mouse.msg == mouse_msg_up || mouse.msg == mouse_msg_move) ) + { + if(ball[iCapture].resmouse(mouse, dx, dy) == 0) + iCapture = -1; + } + } + if (iCapture >= 0) + { + mouse.msg = (mouse_msg_e)0; + ball[iCapture].resmouse(mouse, 0, 0); + } +} + +int main(void) +{ + { + setcodepage(EGE_CODEPAGE_UTF8); + setinitmode(INIT_ANIMATION); + initgraph(LEN, WID); + setcaption("碰撞小球"); + randomize(); + } + int i; + + BALL ball[MAXBALL]; + + for ( ; is_run(); delay_fps(60)) + { + dealMouse(ball); + + for(i = 0; i < MAXBALL; ++i) + ball[i].update(); + + cleardevice(); + + for(i = 0; i < MAXBALL; ++i) + ball[i].drawball(); + + } + + closegraph(); + return 0; +} + diff --git a/cmake_template/ege_demos/graph_new_drawimage.cpp b/cmake_template/ege_demos/graph_new_drawimage.cpp new file mode 100644 index 0000000..9f3e75e --- /dev/null +++ b/cmake_template/ege_demos/graph_new_drawimage.cpp @@ -0,0 +1,86 @@ +//////////////////////////////////////// +/// ege绘图函数和PIMAGE使用 demo +/// @file ege_new_drawimage.cpp +/// @brief 实现绘制白底蓝条纹背景上有四个黄色三角形。 +/// +/// 1. 绘制白底蓝条纹背景。 +/// 2. 绘制第一个三角形,先绘制到PIMAGE变量img上,然后绘制到屏幕。 +/// 3. 绘制第二个三角形,将img的部分(实际上是完整的)绘制到屏幕。 +/// 4. 绘制第三个三角形,通过变换矩阵将img旋转45度后绘制。 +/// 5. 绘制第四个三角形,绘制还原后的img。 +/// +/// @date 2023.6.23 (创建日期: 2020.10.25) +/// +/// +//////////////////////////////////////// + +#include + +int main() +{ + /// 初始化绘图窗口 + initgraph(800, 600); + + // @note Step 1 + // @note 绘制背景 + setbkcolor(WHITE); + setfillcolor(BLUE); + for (int i = 0; i < 12; i++) { + // @note 画无边框填充矩形 + bar(0, i * 50, 800, i * 50 + 20); + } + + // @note Step 2 + // @note 创建一个PIMAGE对象 + PIMAGE img = newimage(200, 200); + // @note 将背景设为透明色 + setbkcolor(EGERGBA(0, 0, 0, 0), img); + setcolor(RED, img); + setfillcolor(YELLOW, img); + + // @note 绘制一个多边形(示例使用三角形) + ege_point points[4]; + points[0].x = 100; + points[0].y = 50; + points[1].x = 50; + points[1].y = 150; + points[2].x = 150; + points[2].y = 150; + // @note 最后一个点需要连回原点以构成封闭多边形,用于绘制边框 + points[3].x = 100; + points[3].y = 50; + // @note 绘制填充多边形到目标图像(为NULL则绘制到屏幕)并绘制边框 + ege_fillpoly(3, points, img); + ege_drawpoly(4, points, img); + // @note 将图像绘制到目标图像上(为NULL则绘制到屏幕),保留透明度信息 + putimage_withalpha(NULL, img, 0, 0); + + // @note Step 3 + // @note 将图像的指定部分绘制到屏幕,从(200,0)点开始绘制到屏幕,到(400,300)点结束 + ege_drawimage(img, 200, 0, 400, 300, 0, 0, 200, 200); + + // @note Step 4 + // @note 通过变换矩阵来绘制图像 + ege_transform_matrix m; + ege_get_transform(&m); + ege_transform_translate(400, 450); + ege_transform_rotate(45.0); + ege_transform_translate(-getwidth(img) / 2, -getheight(img) / 2); + ege_drawimage(img, 0, 0); + + // @note Step 5 + // @note 还原图像 + ege_set_transform(&m); + /* + @note 下面的语句可以清除矩阵: + ege_transform_reset(); + */ + // @note 绘制到窗口 + ege_drawimage(img, 600, 400); + + // @note 暂停程序,等待用户按下任意键继续 + getch(); + delimage(img); + closegraph(); + return 0; +} diff --git a/cmake_template/ege_demos/graph_rotateimage.cpp b/cmake_template/ege_demos/graph_rotateimage.cpp new file mode 100644 index 0000000..081e1bc --- /dev/null +++ b/cmake_template/ege_demos/graph_rotateimage.cpp @@ -0,0 +1,25 @@ +// 旋转图片演示程序 +#include +#include +#include +#include + +int main() +{ + initgraph( 640, 480 ); + setrendermode(RENDER_MANUAL); + PIMAGE img = newimage(); + getimage(img, "JPG", "EGE_LOGO_JPG"); + double r = 0; + fps f; + for ( ; is_run(); delay_fps(60) ) + { + r += 0.02; + if (r > PI * 2) r -= PI * 2; + + cleardevice(); + putimage_rotatezoom(NULL, img, 320, 240, 0.5f, 0.5f, r, 0.5f, 0, -1, 1); + } + return 0; +} + diff --git a/cmake_template/ege_demos/graph_rotatetransparent.cpp b/cmake_template/ege_demos/graph_rotatetransparent.cpp new file mode 100644 index 0000000..697898c --- /dev/null +++ b/cmake_template/ege_demos/graph_rotatetransparent.cpp @@ -0,0 +1,32 @@ +#include + +int main() +{ + int points[6]; + initgraph( 640, 480 ); + // draw background image + setbkcolor(WHITE); + setcolor( BLACK); + setfillcolor( BLACK ); + + bar(50,50,600,400); + + //image to be rotated and put + PIMAGE img = newimage(200,200); + setbkcolor(WHITE,img); + setcolor(RED,img); + setfillcolor(YELLOW,img); + + points[0]=100; + points[1]=50; + points[2]=50; + points[3]=150; + points[4]=150; + points[5]=150; + fillpoly(3,points,img); + + //put and rotate + putimage_rotatetransparent(NULL,img,300,200,100,100,WHITE,45.0/180*PI,2); + getch(); + return 0; +} diff --git a/cmake_template/ege_demos/graph_sort_visualization.cpp b/cmake_template/ege_demos/graph_sort_visualization.cpp new file mode 100644 index 0000000..b3ca42f --- /dev/null +++ b/cmake_template/ege_demos/graph_sort_visualization.cpp @@ -0,0 +1,1430 @@ +/** + * @file graph_sort_visualization.cpp + * @author wysaid (this@xege.org) + * @brief 对一些常见的排序算法进行可视化演示 + * @version 0.1 + * @date 2025-07-12 + * + */ + +#ifndef NOMINMAX +#define NOMINMAX 1 +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 文本本地化宏定义 +#ifdef _MSC_VER +// MSVC编译器使用中文文案 +#define TEXT_WINDOW_TITLE "排序算法可视化演示" +#define TEXT_CONTROLS_TITLE "按键说明:" +#define TEXT_CONTROLS_START "S/空格/回车 - 开始排序当前算法" +#define TEXT_CONTROLS_SHUFFLE "R/ESC - 重新打乱数组" +#define TEXT_CONTROLS_NEXT "-> 切换到下一个算法" +#define TEXT_CONTROLS_PREV "<- 切换到上一个算法" +#define TEXT_CONTROLS_AUTO "A - 自动演示所有算法" +#define TEXT_CONTROLS_SPEED "+/= - 加速动画,-/_ - 减速动画" +#define TEXT_CONTROLS_EXIT "ESC - 退出程序(排序中按ESC可中断)" +#define TEXT_CURRENT_ALGORITHM "当前算法: %s" +#define TEXT_ARRAY_SORTED "数组已排序" +#define TEXT_ARRAY_NOT_SORTED "数组未排序" +#define TEXT_SPEED_INFO "动画速度设置: %d ms 每次操作 (+ 加速, - 减速)" +#define TEXT_EXECUTING "正在执行: %s" +#define TEXT_EXECUTING_BATCH "正在执行: %s (%d/%d)" +#define TEXT_SORT_COMPLETE "排序完成!" +#define TEXT_SORT_TIME "排序用时: %ld 毫秒" +#define TEXT_SORT_TIME_SHORT "用时: %ld ms" +#define TEXT_ARRAY_SHUFFLED "数组已重新打乱" +#define TEXT_NEXT_ALGORITHM "2秒后继续下一个算法..." +#define TEXT_SORTING_INTERRUPTED "排序已中断" +#define TEXT_READY_TO_SORT "准备就绪,按 S 开始排序" +#define TEXT_SORTING_IN_PROGRESS "排序进行中..." +#define TEXT_AUTO_DEMO_COMPLETE "自动演示完成!" +#define TEXT_BUBBLE_SORT "冒泡排序" +#define TEXT_SELECTION_SORT "选择排序" +#define TEXT_INSERTION_SORT "插入排序" +#define TEXT_QUICK_SORT "快速排序" +#define TEXT_MERGE_SORT "归并排序" +#define TEXT_HEAP_SORT "堆排序" +#define TEXT_SHELL_SORT "希尔排序" +#define TEXT_RADIX_SORT "基数排序" +#define TEXT_COUNTING_SORT "计数排序" +#define TEXT_STD_SORT "标准库排序" +#define TEXT_STD_STABLE_SORT "标准库稳定排序" + +#define TEXT_BUBBLE_COMPLEXITY "冒泡排序 - 时间复杂度: O(n^2), 空间复杂度: O(1)" +#define TEXT_SELECTION_COMPLEXITY "选择排序 - 时间复杂度: O(n^2), 空间复杂度: O(1)" +#define TEXT_INSERTION_COMPLEXITY "插入排序 - 时间复杂度: O(n^2), 空间复杂度: O(1)" +#define TEXT_QUICK_COMPLEXITY "快速排序 - 时间复杂度: O(n log n), 空间复杂度: O(log n)" +#define TEXT_MERGE_COMPLEXITY "归并排序 - 时间复杂度: O(n log n), 空间复杂度: O(n)" +#define TEXT_HEAP_COMPLEXITY "堆排序 - 时间复杂度: O(n log n), 空间复杂度: O(1)" +#define TEXT_SHELL_COMPLEXITY "希尔排序 - 时间复杂度: O(n^1.5), 空间复杂度: O(1)" +#define TEXT_RADIX_COMPLEXITY "基数排序 - 时间复杂度: O(kn), 空间复杂度: O(k+n)" +#define TEXT_COUNTING_COMPLEXITY "计数排序 - 时间复杂度: O(n+k), 空间复杂度: O(k)" +#define TEXT_STD_COMPLEXITY "标准库排序 - 通常为混合排序算法" +#define TEXT_STD_STABLE_COMPLEXITY "标准库稳定排序 - 时间复杂度: O(n log n), 空间复杂度: O(n)" +#define TEXT_STD_PARTIAL_COMPLEXITY "标准库部分排序 - 时间复杂度: O(n log k), 空间复杂度: O(1)" +#define TEXT_STD_MAKE_HEAP_COMPLEXITY "标准库堆排序 - 时间复杂度: O(n log n), 空间复杂度: O(1)" +#define TEXT_STATS_FORMAT "当前算法: %s | 写入/赋值: %d 次 | 比较: %d 次 | 读取: %d 次 | 按 ESC 可中断排序" +#define TEXT_FONT_NAME "宋体" +#else +// 非MSVC编译器使用英文文案 +#define TEXT_WINDOW_TITLE "Sort Algorithm Visualization" +#define TEXT_CONTROLS_TITLE "Controls:" +#define TEXT_CONTROLS_START "S/Space/Enter - Start sorting current algorithm" +#define TEXT_CONTROLS_SHUFFLE "R/ESC - Shuffle array" +#define TEXT_CONTROLS_NEXT "-> Switch to next algorithm" +#define TEXT_CONTROLS_PREV "<- Switch to previous algorithm" +#define TEXT_CONTROLS_AUTO "A - Auto demo all algorithms" +#define TEXT_CONTROLS_SPEED "+/= - Speed up animation, -/_ - Slow down animation" +#define TEXT_CONTROLS_EXIT "ESC - Exit program (Press ESC during sorting to interrupt)" +#define TEXT_CURRENT_ALGORITHM "Current Algorithm: %s" +#define TEXT_ARRAY_SORTED "Array is sorted" +#define TEXT_ARRAY_NOT_SORTED "Array is not sorted" +#define TEXT_SPEED_INFO "Animation speed: %d ms per operation (+ faster, - slower)" +#define TEXT_EXECUTING "Executing: %s" +#define TEXT_EXECUTING_BATCH "Executing: %s (%d/%d)" +#define TEXT_SORT_COMPLETE "Sort Complete!" +#define TEXT_SORT_TIME "Sort Time: %ld ms" +#define TEXT_SORT_TIME_SHORT "Time: %ld ms" +#define TEXT_ARRAY_SHUFFLED "Array shuffled" +#define TEXT_NEXT_ALGORITHM "Next algorithm in 2 seconds..." +#define TEXT_SORTING_INTERRUPTED "Sorting interrupted" +#define TEXT_READY_TO_SORT "Ready to sort, press S to start" +#define TEXT_SORTING_IN_PROGRESS "Sorting in progress..." +#define TEXT_AUTO_DEMO_COMPLETE "Auto demo complete!" +#define TEXT_BUBBLE_SORT "Bubble Sort" +#define TEXT_SELECTION_SORT "Selection Sort" +#define TEXT_INSERTION_SORT "Insertion Sort" +#define TEXT_QUICK_SORT "Quick Sort" +#define TEXT_MERGE_SORT "Merge Sort" +#define TEXT_HEAP_SORT "Heap Sort" +#define TEXT_SHELL_SORT "Shell Sort" +#define TEXT_RADIX_SORT "Radix Sort" +#define TEXT_COUNTING_SORT "Counting Sort" +#define TEXT_STD_SORT "Standard Sort" +#define TEXT_STD_STABLE_SORT "Standard Stable Sort" +#define TEXT_BUBBLE_COMPLEXITY "Bubble Sort - Time: O(n^2), Space: O(1)" +#define TEXT_SELECTION_COMPLEXITY "Selection Sort - Time: O(n^2), Space: O(1)" +#define TEXT_INSERTION_COMPLEXITY "Insertion Sort - Time: O(n^2), Space: O(1)" +#define TEXT_QUICK_COMPLEXITY "Quick Sort - Time: O(n log n), Space: O(log n)" +#define TEXT_MERGE_COMPLEXITY "Merge Sort - Time: O(n log n), Space: O(n)" +#define TEXT_HEAP_COMPLEXITY "Heap Sort - Time: O(n log n), Space: O(1)" +#define TEXT_SHELL_COMPLEXITY "Shell Sort - Time: O(n^1.5), Space: O(1)" +#define TEXT_RADIX_COMPLEXITY "Radix Sort - Time: O(kn), Space: O(k+n)" +#define TEXT_COUNTING_COMPLEXITY "Counting Sort - Time: O(n+k), Space: O(k)" + +#define TEXT_STD_COMPLEXITY "Standard Sort - Usually hybrid algorithm" +#define TEXT_STD_STABLE_COMPLEXITY "Standard Stable Sort - Time: O(n log n), Space: O(n)" + +#define TEXT_STATS_FORMAT \ + "Algorithm: %s | Writes/Assigns: %d | Comparisons: %d | Reads: %d | Press ESC to interrupt sorting" +#define TEXT_FONT_NAME "Arial" +#endif + +// 可视化参数 +const int WINDOW_WIDTH = 1200; +const int WINDOW_HEIGHT = 800; +const int ARRAY_SIZE = 100; +const int BAR_WIDTH = WINDOW_WIDTH / ARRAY_SIZE; + +static int g_operationDelay = 50; // 用于调整每次操作(赋值、访问等)的动画延迟时间 + +// 前向声明 +class VisualizationState; + +/** + * @class MyElement + * @brief 用于排序可视化的元素类 + * + * 这个类封装了排序元素,能够自动统计赋值、拷贝、交换等操作 + * 并在操作时触发可视化更新 + * + * 统计计数说明: + * - 写入/赋值次数:包括构造函数初始化、赋值操作符调用 + * - 读取次数:包括类型转换操作、getValue()调用等读取数据的操作 + * - 比较次数:包括所有比较操作符调用 + * - 交换操作:通过std::swap实现,包含1次读取+2次写入 + */ +class MyElement +{ +public: + // 默认构造函数 + MyElement() : m_value(0), m_visualizerPtr(nullptr) {} + + // 带值构造函数 + MyElement(int value) : m_value(value), m_visualizerPtr(nullptr) + { + recordWrite(); // 构造函数初始化值算作写入操作 + notifyVisualization(); + if (g_operationDelay > 0 && m_enableVisualization) { + api_sleep(g_operationDelay); + } + } + + // 拷贝构造函数 + MyElement(const MyElement& other) : m_value(other.getValue()), m_visualizerPtr(other.m_visualizerPtr) + { + recordWrite(); // 拷贝构造函数算作写入操作 + notifyVisualization(); + if (g_operationDelay > 0 && m_enableVisualization) { + api_sleep(g_operationDelay); + } + } + + // 赋值操作符 + MyElement& operator=(const MyElement& other) + { + if (this != &other) { + m_value = other.getValue(); + m_visualizerPtr = other.m_visualizerPtr; + recordWrite(); // 赋值算作一种数据写入 + notifyVisualization(); + if (g_operationDelay > 0 && m_enableVisualization) { + api_sleep(g_operationDelay); + } + } + return *this; + } + + // 从 int 赋值 + MyElement& operator=(int value) + { + m_value = value; + recordWrite(); + notifyVisualization(); + if (g_operationDelay > 0 && m_enableVisualization) { + api_sleep(g_operationDelay); + } + return *this; + } + + // 比较操作符 + bool operator<(const MyElement& other) const + { + recordComparison(); + return m_value < other.m_value; + } + + bool operator>(const MyElement& other) const + { + recordComparison(); + return m_value > other.m_value; + } + + bool operator<=(const MyElement& other) const + { + recordComparison(); + return m_value <= other.m_value; + } + + bool operator>=(const MyElement& other) const + { + recordComparison(); + return m_value >= other.m_value; + } + + bool operator==(const MyElement& other) const + { + recordComparison(); + return m_value == other.m_value; + } + + bool operator!=(const MyElement& other) const + { + recordComparison(); + return m_value != other.m_value; + } + + // 与 int 的比较操作 + bool operator<(int value) const + { + recordComparison(); + return m_value < value; + } + + bool operator>(int value) const + { + recordComparison(); + return m_value > value; + } + + bool operator<=(int value) const + { + recordComparison(); + return m_value <= value; + } + + bool operator>=(int value) const + { + recordComparison(); + return m_value >= value; + } + + bool operator==(int value) const + { + recordComparison(); + return m_value == value; + } + + bool operator!=(int value) const + { + recordComparison(); + return m_value != value; + } + + // 类型转换操作符 + operator int() const + { + recordAccess(); + return m_value; + } + + // 获取值 + int getValue() const + { + recordAccess(); + return m_value; + } + + // 设置可视化器 + void setVisualizer(VisualizationState* visualizer) { m_visualizerPtr = visualizer; } + + // 获取可视化器 + VisualizationState* getVisualizer() const { return m_visualizerPtr; } + + // 统计相关的静态方法 + static void resetStats() + { + s_writeCount = 0; + s_compareCount = 0; + s_readCount = 0; + } + + static int getWriteCount() { return s_writeCount; } + + static int getCompareCount() { return s_compareCount; } + + static int getReadCount() { return s_readCount; } + + static bool visualizationEnabled() { return m_enableVisualization; } + + static void enableVisualization(bool enable) { m_enableVisualization = enable; } + +private: + int m_value; + VisualizationState* m_visualizerPtr; + + // 静态统计变量 + static int s_writeCount; + static int s_compareCount; + static int s_readCount; // 读取次数 + static bool m_enableVisualization; // 是否启用统计 + + // 统计方法 + void recordWrite() const + { + if (m_enableVisualization) { + ++s_writeCount; + } + } + + void recordComparison() const + { + if (m_enableVisualization) { + ++s_compareCount; + ++s_readCount; // 比较操作会读取元素值 + } + } + + void recordAccess() const + { + if (m_enableVisualization) { + ++s_readCount; // 记录读取次数 + } + } + + // 通知可视化更新 + void notifyVisualization() const; +}; + +// 静态成员变量定义 +int MyElement::s_writeCount = 0; +int MyElement::s_compareCount = 0; +int MyElement::s_readCount = 0; +bool MyElement::m_enableVisualization = false; + +// 为 MyElement 添加 swap 特化 +namespace std +{ +template <> void swap(MyElement& a, MyElement& b) +{ + MyElement temp = a; // 拷贝构造函数:创建临时对象(1次写入) + + a = b; // 赋值操作符:将b的值写入a(1次写入) + b = temp; // 赋值操作符:将temp的值写入b(1次写入) + + // 总计:3次写入 - 这正确反映了swap的实际操作成本 + // 注意:读取操作应该通过元素的访问操作来统计 +} +} // namespace std + +/** + * @class MyArray + * @brief 用于排序可视化的数组类 + * + * 这个类封装了用于排序的数组,并提供可视化功能 + * 支持任意排序算法的可视化,通过自定义迭代器实现 + */ +class MyArray +{ +public: + /** + * @class MyIterator + * @brief 自定义迭代器,用于在排序过程中进行可视化 + * + * 这个迭代器包装了vector的迭代器,在进行交换和比较操作时 + * 会触发可视化更新 + */ + class MyIterator + { + public: + // 迭代器类型定义 + using iterator_category = std::random_access_iterator_tag; + using value_type = MyElement; + using difference_type = std::ptrdiff_t; + using pointer = MyElement*; + using reference = MyElement&; + + MyIterator(MyArray* arr, int idx) : m_arrayPtr(arr), m_index(idx) {} + + // 解引用操作 + reference operator*() + { + m_arrayPtr->m_highlightedIndex1 = m_index; + m_arrayPtr->notifyVisualization(); + return m_arrayPtr->m_data[m_index]; + } + + // const 版本的解引用操作 + const reference operator*() const + { + m_arrayPtr->m_highlightedIndex1 = m_index; + m_arrayPtr->notifyVisualization(); + return m_arrayPtr->m_data[m_index]; + } + + pointer operator->() { return &(m_arrayPtr->m_data[m_index]); } + + const pointer operator->() const { return &(m_arrayPtr->m_data[m_index]); } + + // 前置递增 + MyIterator& operator++() + { + ++m_index; + return *this; + } + + // 后置递增 + MyIterator operator++(int) + { + MyIterator temp = *this; + ++m_index; + return temp; + } + + // 前置递减 + MyIterator& operator--() + { + --m_index; + return *this; + } + + // 后置递减 + MyIterator operator--(int) + { + MyIterator temp = *this; + --m_index; + return temp; + } + + // 随机访问操作 + MyIterator operator+(difference_type n) const { return MyIterator(m_arrayPtr, m_index + n); } + + MyIterator operator-(difference_type n) const { return MyIterator(m_arrayPtr, m_index - n); } + + MyIterator& operator+=(difference_type n) + { + m_index += n; + return *this; + } + + MyIterator& operator-=(difference_type n) + { + m_index -= n; + return *this; + } + + difference_type operator-(const MyIterator& other) const { return m_index - other.m_index; } + + // 下标操作 + reference operator[](difference_type n) + { + m_arrayPtr->m_highlightedIndex1 = m_index + n; + m_arrayPtr->notifyVisualization(); + return m_arrayPtr->m_data[m_index + n]; + } + + // const 版本的下标操作 + const reference operator[](difference_type n) const + { + m_arrayPtr->m_highlightedIndex1 = m_index + n; + m_arrayPtr->notifyVisualization(); + return m_arrayPtr->m_data[m_index + n]; + } + + // 比较操作 + bool operator==(const MyIterator& other) const { return m_index == other.m_index; } + + bool operator!=(const MyIterator& other) const { return m_index != other.m_index; } + + bool operator<(const MyIterator& other) const { return m_index < other.m_index; } + + bool operator>(const MyIterator& other) const { return m_index > other.m_index; } + + bool operator<=(const MyIterator& other) const { return m_index <= other.m_index; } + + bool operator>=(const MyIterator& other) const { return m_index >= other.m_index; } + + // 获取索引(用于调试) + int getIndex() const { return m_index; } + + // 获取数组指针(用于交换函数) + MyArray* getArray() const { return m_arrayPtr; } + + private: + MyArray* m_arrayPtr; + int m_index; + }; + + // 构造函数 + MyArray(int size = ARRAY_SIZE) : m_data(size), m_visualizerPtr(nullptr) + { + // 初始化数组为随机值 + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis(10, WINDOW_HEIGHT - 50); + + MyElement::enableVisualization(false); + + for (size_t i = 0; i < static_cast(size); ++i) { + m_data[i] = MyElement(dis(gen)); + m_data[i].setVisualizer(nullptr); // 稍后会设置 + } + + MyElement::enableVisualization(true); + } + + // 设置可视化状态引用 + void setVisualizer(VisualizationState* visualizer) + { + m_visualizerPtr = visualizer; + // 为所有元素设置可视化器 + for (auto& element : m_data) { + element.setVisualizer(visualizer); + } + } + + // 获取迭代器 + MyIterator begin() { return MyIterator(this, 0); } + + MyIterator end() { return MyIterator(this, m_data.size()); } + + // 获取大小 + size_t size() const { return m_data.size(); } + + // 获取数组数据(用于可视化) + const std::vector& getData() const { return m_data; } + + // 获取高亮索引(用于可视化) + int getHighlightedIndex1() const { return m_highlightedIndex1; } + + int getHighlightedIndex2() const { return m_highlightedIndex2; } + + // 获取当前算法名称(用于可视化) + const std::string& getCurrentAlgorithm() const { return m_currentAlgorithm; } + + // 清除高亮(由可视化类调用) + void clearHighlight() + { + m_highlightedIndex1 = -1; + m_highlightedIndex2 = -1; + } + + // 打乱数组 + void shuffle() + { + std::random_device rd; + std::mt19937 gen(rd()); + std::shuffle(m_data.begin(), m_data.end(), gen); + resetStats(); // 重置统计信息 + notifyVisualization(); + } + + // 检查是否已排序 + bool isSorted() const + { + for (size_t i = 1; i < m_data.size(); ++i) { + if (m_data[i].getValue() < m_data[i - 1].getValue()) { + return false; + } + } + return true; + } + + // 重置统计信息 + void resetStats() + { + clearHighlight(); + MyElement::resetStats(); + } + + // 设置当前算法名称 + void setCurrentAlgorithm(const std::string& algorithmName) + { + m_currentAlgorithm = algorithmName; + resetStats(); + } + + // 获取统计信息 + int getWriteCount() const { return MyElement::getWriteCount(); } + + int getCompareCount() const { return MyElement::getCompareCount(); } + + int getReadCount() const { return MyElement::getReadCount(); } + + // 高亮两个元素(用于比较可视化) + void highlightElements(int idx1, int idx2 = -1) + { + m_highlightedIndex1 = idx1; + m_highlightedIndex2 = idx2; + } + + // 通知可视化更新 + void notifyVisualization(); + +private: + std::vector m_data; + + int m_highlightedIndex1 = -1; // 高亮的第一个元素 + int m_highlightedIndex2 = -1; // 高亮的第二个元素 + + VisualizationState* m_visualizerPtr = nullptr; // 可视化状态引用 + + std::string m_currentAlgorithm = ""; // 当前算法名称 +}; + +// 为MyArray的迭代器添加交换函数特化 +namespace std +{ +template <> void iter_swap(MyArray::MyIterator a, MyArray::MyIterator b) +{ + // 高亮要交换的元素 + MyArray* arr = a.getArray(); + if (arr && a.getIndex() < arr->size() && b.getIndex() < arr->size()) { + arr->highlightElements(a.getIndex(), b.getIndex()); + } + + // 执行交换 - MyElement 的赋值操作符会自动处理统计和可视化 + std::swap(*a, *b); +} +} // namespace std + +// 排序算法实现 + +/** + * @brief 冒泡排序 + */ +template void bubbleSort(Iterator first, Iterator last) +{ + for (auto i = first; i != last; ++i) { + for (auto j = first; j != last - 1; ++j) { + if (*j > *(j + 1)) { + std::iter_swap(j, j + 1); + } + } + } +} + +/** + * @brief 选择排序 + */ +template void selectionSort(Iterator first, Iterator last) +{ + for (auto i = first; i != last; ++i) { + auto minIter = i; + for (auto j = i + 1; j != last; ++j) { + if (*j < *minIter) { + minIter = j; + } + } + if (minIter != i) { + std::iter_swap(i, minIter); + } + } +} + +/** + * @brief 插入排序 + */ +template void insertionSort(Iterator first, Iterator last) +{ + auto* arr = first.getArray(); + + for (auto i = first + 1; i != last; ++i) { + auto key = *i; + auto j = i - 1; + + while (j >= first && *j > key) { + arr->highlightElements(j - first, j - first + 1); // 高亮交换的元素 + *(j + 1) = *j; + --j; + } + *(j + 1) = key; + } + arr->clearHighlight(); // 清除高亮 +} + +/** + * @brief 快速排序辅助函数 - 分区 + */ +template Iterator partition(Iterator first, Iterator last) +{ + auto pivot = *(last - 1); + auto i = first - 1; + + for (auto j = first; j != last - 1; ++j) { + if (*j <= pivot) { + ++i; + if (i != j) { + std::iter_swap(i, j); + } + } + } + std::iter_swap(i + 1, last - 1); + return i + 1; +} + +/** + * @brief 快速排序 + */ +template void quickSort(Iterator first, Iterator last) +{ + if (first < last) { + auto pivot = partition(first, last); + quickSort(first, pivot); + quickSort(pivot + 1, last); + } +} + +/** + * @brief 归并排序辅助函数 - 合并 + */ +template void merge(Iterator first, Iterator mid, Iterator last) +{ + std::vector> temp; + + auto left = first; + auto right = mid; + + while (left != mid && right != last) { + if (*left <= *right) { + temp.emplace_back(left, *left); + ++left; + } else { + temp.emplace_back(right, *right); + ++right; + } + } + + while (left != mid) { + temp.emplace_back(left, *left); + ++left; + } + + while (right != last) { + temp.emplace_back(right, *right); + ++right; + } + + auto* arr = first.getArray(); + + // 复制回原数组 + auto iter = first; + for (const auto& val : temp) { + arr->highlightElements(iter.getIndex(), val.first.getIndex()); // 高亮当前元素 + *iter = val.second; + ++iter; + } +} + +/** + * @brief 归并排序 + */ +template void mergeSort(Iterator first, Iterator last) +{ + auto distance = std::distance(first, last); + if (distance > 1) { + auto mid = first + distance / 2; + mergeSort(first, mid); + mergeSort(mid, last); + merge(first, mid, last); + } +} + +/** + * @brief 堆排序 + */ +template void heapSort(Iterator first, Iterator last) +{ + // 手动实现堆排序以便与自定义迭代器兼容 + auto heapify = [](Iterator start, Iterator end, Iterator root) { + while (true) { + auto largest = root; + auto left = start + 2 * (root - start) + 1; + auto right = start + 2 * (root - start) + 2; + + if (left < end && (*left) > *largest) { + largest = left; + } + + if (right < end && (*right) > *largest) { + largest = right; + } + + if (largest == root) { + break; + } + + std::iter_swap(root, largest); + root = largest; + } + }; + + // 构建堆 + auto distance = std::distance(first, last); + for (int i = distance / 2 - 1; i >= 0; i--) { + heapify(first, last, first + i); + } + + // 提取元素 + for (auto it = last - 1; it != first; --it) { + std::iter_swap(first, it); + heapify(first, it, first); + } +} + +/** + * @brief 希尔排序 + */ +template void shellSort(Iterator first, Iterator last) +{ + auto distance = std::distance(first, last); + if (distance < 2) { + return; + } + + auto* arr = first.getArray(); + + // 使用经典的希尔排序间隔序列 + for (int gap = distance / 2; gap > 0; gap /= 2) { + for (auto i = first + gap; i != last; ++i) { + auto temp = *i; + auto j = i; + + while (j >= first + gap && *(j - gap) > temp) { + arr->highlightElements(j.getIndex(), (j - gap).getIndex()); // 高亮交换的元素 + *j = *(j - gap); + j -= gap; + } + *j = temp; + } + } +} + +/** + * @brief 基数排序(仅适用于非负整数) + */ +template void radixSort(Iterator first, Iterator last) +{ + if (first == last) { + return; + } + + // 找到最大值以确定位数 + auto max_val = *std::max_element(first, last); + auto* arr = first.getArray(); + + std::vector count(10, 0); + std::vector> output; + + // 对每一位进行计数排序 + for (int exp = 1; max_val.getValue() / exp > 0; exp *= 10) { + MyElement::enableVisualization(false); // 禁用可视化以避免干扰 + output.resize(std::distance(first, last), {first, MyElement(0)}); // 确保输出数组大小正确 + count.assign(10, 0); // 重置计数器 + MyElement::enableVisualization(true); // 恢复可视化 + + // 计算每个数字的频率 + for (auto it = first; it != last; ++it) { + count[(it->getValue() / exp) % 10]++; + } + + // 计算累积计数 + for (int i = 1; i < 10; i++) { + count[i] += count[i - 1]; + } + + // 构建输出数组 + int index = std::distance(first, last) - 1; + for (auto it = last - 1; it >= first; --it, --index) { + output[count[(it->getValue() / exp) % 10] - 1] = std::make_pair(it, *it); + count[(it->getValue() / exp) % 10]--; + } + + // 将输出数组复制回原数组 + index = 0; + for (auto it = first; it != last; ++it, ++index) { + auto& elem = output[index]; + arr->highlightElements(it.getIndex(), elem.first.getIndex()); // 高亮当前元素 + *it = elem.second; + } + } +} + +/** + * @brief 计数排序 + */ +template void countingSort(Iterator first, Iterator last) +{ + if (first == last) { + return; + } + + auto* arr = first.getArray(); + + // 找到最大值和最小值 + auto min_val = std::min_element(first, last)->getValue(); + auto max_val = std::max_element(first, last)->getValue(); + + int range = max_val - min_val + 1; + + std::vector count(range, 0); + + // 计算每个元素的频率 + for (auto it = first; it != last; ++it) { + int index = it->getValue() - min_val; + arr->highlightElements(it.getIndex()); // 高亮当前元素 + arr->notifyVisualization(); + count[it->getValue() - min_val]++; + } + + // 重建数组 + auto current = first; + for (int i = 0; i < range; i++) { + while (count[i]-- > 0) { + *current = typename std::iterator_traits::value_type(i + min_val); + ++current; + } + } +} + +// 排序算法信息结构 +struct SortAlgorithm +{ + std::string name; + std::function func; +}; + +/** + * @class VisualizationState + * @brief 管理排序可视化的状态和界面显示 + * + * 这个类封装了排序可视化演示的所有状态,包括数组、算法列表、 + * 当前算法索引等,并提供相应的操作方法 + */ +class VisualizationState +{ +public: + /** + * @brief 构造函数,初始化可视化状态 + */ + VisualizationState() : m_array(ARRAY_SIZE), m_currentAlgorithm(0), m_sorting(false) + { + // 算法列表初始为空,通过addAlgorithm方法动态添加 + m_array.setVisualizer(this); + } + + /** + * @brief 添加排序算法 + * @param name 算法名称 + * @param func 算法函数 + */ + void addAlgorithm(const std::string& name, std::function func) + { + m_algorithms.push_back({name, func}); + } + + /** + * @brief 绘制可视化界面 + * @param updateScene 是否更新场景 + */ + void updateScene(bool flush = true) + { + MyElement::enableVisualization(false); // 禁用统计 + + handleKeyMsg(); + cleardevice(); + + // 绘制标题和状态信息 + setcolor(WHITE); + setfont(20, 0, TEXT_FONT_NAME); + + // 主标题 + outtextxy(10, 10, TEXT_WINDOW_TITLE); + + // 状态信息 + setfont(14, 0, TEXT_FONT_NAME); + setcolor(CYAN); + + xyprintf(10, 40, TEXT_STATS_FORMAT, m_array.getCurrentAlgorithm().c_str(), m_array.getWriteCount(), + m_array.getCompareCount(), m_array.getReadCount()); + + // 显示速度信息 + setcolor(LIGHTGREEN); + xyprintf(10, 65, TEXT_SPEED_INFO, g_operationDelay); + + // 获取数组数据 + const auto& data = m_array.getData(); + + setcolor(BLACK); + setfont(12, 0, "Arial"); + + // 绘制数组元素 + for (int i = 0; i < data.size(); ++i) { + int x = i * BAR_WIDTH; + int height = data[i].getValue(); // 使用 getValue() 获取数值 + + // 设置颜色 + if (i == m_array.getHighlightedIndex1()) { + setfillcolor(RED); // 当前访问的元素 + } else if (i == m_array.getHighlightedIndex2()) { + setfillcolor(YELLOW); // 第二个比较元素 + } else { + setfillcolor(LIGHTBLUE); // 普通元素 + } + + // 绘制矩形条 + bar(x, WINDOW_HEIGHT - height - 20, x + BAR_WIDTH - 2, WINDOW_HEIGHT - 20); + + // 可选:绘制数值(只在条足够宽时显示) + if (BAR_WIDTH > 20) { + int text_x = x + BAR_WIDTH / 2; + int text_y = WINDOW_HEIGHT - height / 2 - 10; + if (text_y > WINDOW_HEIGHT - height - 20 + 5) { + xyprintf(text_x - 10, text_y, "%d", data[i].getValue()); // 减去10来大致居中 + } + } + } + + // 清除高亮 + // m_array.clearHighlight(); + + if (flush) { + delay_ms(1); + } + + MyElement::enableVisualization(true); + } + + /** + * @brief 显示界面 + */ + void showInterface() + { + updateScene(false); + + MyElement::enableVisualization(false); + + // 显示说明 + setcolor(WHITE); + setfont(16, 0, TEXT_FONT_NAME); + outtextxy(10, 50, TEXT_CONTROLS_TITLE); + outtextxy(10, 80, TEXT_CONTROLS_START); + outtextxy(10, 110, TEXT_CONTROLS_SHUFFLE); + outtextxy(10, 140, TEXT_CONTROLS_NEXT); + outtextxy(10, 170, TEXT_CONTROLS_PREV); + outtextxy(10, 200, TEXT_CONTROLS_AUTO); + outtextxy(10, 230, TEXT_CONTROLS_SPEED); + outtextxy(10, 260, TEXT_CONTROLS_EXIT); + + // 显示当前算法(如果有算法的话) + if (!m_algorithms.empty()) { + setcolor(YELLOW); + setfont(18, 0, TEXT_FONT_NAME); + xyprintf(10, 300, TEXT_CURRENT_ALGORITHM, m_algorithms[m_currentAlgorithm].name.c_str()); + } + + // 显示速度信息 + setcolor(CYAN); + setfont(14, 0, TEXT_FONT_NAME); + xyprintf(10, 330, TEXT_SPEED_INFO, g_operationDelay); + + if (m_array.isSorted()) { + setcolor(GREEN); + outtextxy(10, 360, TEXT_ARRAY_SORTED); + } else { + setcolor(RED); + outtextxy(10, 360, TEXT_ARRAY_NOT_SORTED); + } + + delay_fps(60); + MyElement::enableVisualization(true); + } + + /** + * @brief 开始排序当前算法 + */ + void startSorting() + { + if (m_sorting || m_algorithms.empty()) { + return; + } + + if (m_array.isSorted()) { + shuffleArray(); + } + + m_sorting = true; + + // 设置当前算法名称并重置统计 + m_array.setCurrentAlgorithm(m_algorithms[m_currentAlgorithm].name); + + cleardevice(); + setcolor(YELLOW); + setfont(24, 0, TEXT_FONT_NAME); + xyprintf(10, 10, TEXT_EXECUTING, m_algorithms[m_currentAlgorithm].name.c_str()); + + // 显示算法信息 + setcolor(WHITE); + setfont(14, 0, TEXT_FONT_NAME); + switch (m_currentAlgorithm) { + case 0: + outtextxy(10, 50, TEXT_BUBBLE_COMPLEXITY); + break; + case 1: + outtextxy(10, 50, TEXT_SELECTION_COMPLEXITY); + break; + case 2: + outtextxy(10, 50, TEXT_INSERTION_COMPLEXITY); + break; + case 3: + outtextxy(10, 50, TEXT_QUICK_COMPLEXITY); + break; + case 4: + outtextxy(10, 50, TEXT_MERGE_COMPLEXITY); + break; + case 5: + outtextxy(10, 50, TEXT_HEAP_COMPLEXITY); + break; + case 6: + outtextxy(10, 50, TEXT_SHELL_COMPLEXITY); + break; + case 7: + outtextxy(10, 50, TEXT_RADIX_COMPLEXITY); + break; + case 8: + outtextxy(10, 50, TEXT_COUNTING_COMPLEXITY); + break; + case 9: + outtextxy(10, 50, TEXT_STD_COMPLEXITY); + break; + case 10: + outtextxy(10, 50, TEXT_STD_STABLE_COMPLEXITY); + break; + default: + outtextxy(10, 50, TEXT_STD_COMPLEXITY); + break; + } + + auto startTime = std::chrono::high_resolution_clock::now(); + bool success = false; + try { + m_algorithms[m_currentAlgorithm].func(m_array.begin(), m_array.end()); + success = true; + } + catch (...) { + } + + m_sorting = false; + + if (success) { + auto endTime = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(endTime - startTime); + + setcolor(GREEN); + setfont(20, 0, TEXT_FONT_NAME); + outtextxy(10, 80, TEXT_SORT_COMPLETE); + + setcolor(CYAN); + setfont(16, 0, TEXT_FONT_NAME); + xyprintf(10, 110, TEXT_SORT_TIME, duration.count()); + } else { + setcolor(RED); + setfont(20, 0, TEXT_FONT_NAME); + outtextxy(10, 80, TEXT_SORTING_INTERRUPTED); + } + + updateScene(); + } + + /** + * @brief 打乱数组 + */ + void shuffleArray() + { + if (m_sorting) { + return; + } + + bool enableVisualization = MyElement::visualizationEnabled(); + MyElement::enableVisualization(false); // 禁用统计以避免干扰 + m_array.shuffle(); + setcolor(CYAN); + setfont(16, 0, TEXT_FONT_NAME); + outtextxy(10, 360, TEXT_ARRAY_SHUFFLED); + MyElement::enableVisualization(enableVisualization); // 恢复统计状态 + } + + /** + * @brief 切换到下一个算法 + */ + void nextAlgorithm() + { + if (m_sorting || m_algorithms.empty()) { + return; + } + m_currentAlgorithm = (m_currentAlgorithm + 1) % m_algorithms.size(); + } + + /** + * @brief 切换到上一个算法 + */ + void previousAlgorithm() + { + if (m_sorting || m_algorithms.empty()) { + return; + } + m_currentAlgorithm = (m_currentAlgorithm - 1 + m_algorithms.size()) % m_algorithms.size(); + } + + /** + * @brief 自动演示所有算法 + */ + void autoDemo() + { + if (m_sorting || m_algorithms.empty()) { + return; + } + + m_sorting = true; + for (int i = 0; i < m_algorithms.size(); ++i) { + m_array.shuffle(); + + // 设置当前算法名称并重置统计 + m_array.setCurrentAlgorithm(m_algorithms[i].name); + + cleardevice(); + setcolor(YELLOW); + setfont(24, 0, TEXT_FONT_NAME); + xyprintf(10, 10, TEXT_EXECUTING_BATCH, m_algorithms[i].name.c_str(), i + 1, (int)m_algorithms.size()); + + auto startTime = std::chrono::high_resolution_clock::now(); + m_algorithms[i].func(m_array.begin(), m_array.end()); + auto endTime = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(endTime - startTime); + + setcolor(GREEN); + setfont(20, 0, TEXT_FONT_NAME); + outtextxy(10, 50, TEXT_SORT_COMPLETE); + + setcolor(CYAN); + setfont(16, 0, TEXT_FONT_NAME); + xyprintf(10, 80, TEXT_SORT_TIME_SHORT, duration.count()); + + updateScene(); + + if (i < m_algorithms.size() - 1) { + setcolor(WHITE); + outtextxy(10, 120, TEXT_NEXT_ALGORITHM); + } else { + setcolor(LIGHTGREEN); + setfont(18, 0, TEXT_FONT_NAME); + outtextxy(10, 120, TEXT_AUTO_DEMO_COMPLETE); + } + } + m_sorting = false; + } + + /** + * @brief 处理键盘输入 + * @param ch 输入的字符 + * @return 是否继续运行(false表示退出) + */ + bool handleInput(char ch) + { + switch (ch) { + case '+': + case '=': + g_operationDelay = std::max(0, g_operationDelay - 10); // 减少操作延迟时间(加速动画) + return true; + case '-': + case '_': + g_operationDelay = std::min(500, g_operationDelay + 10); // 增加操作延迟时间(减速动画) + return true; + default: + break; + } + + if (m_sorting) { + if (ch == 27) { // ESC键 + throw std::runtime_error("Sorting interrupted by user."); // 用户中断排序 + } + return true; // 排序进行中,忽略其他按键输入 + } + + switch (ch) { + case ' ': // 空格键 + case '\n': // 回车键 + case '\r': // 回车键 + case 's': // S键 + case 'S': // S键(大写) + startSorting(); + break; + + case 27: // ESC键 - 退出或重新打乱 + case 'r': // R键 + case 'R': // R键(大写) + shuffleArray(); + break; + + case 'n': // N键 + case 'N': // N键(大写) + case 0x27: // 右箭头键 + case 0x28: // 下箭头键 + nextAlgorithm(); + break; + + case 'p': // P键 + case 'P': // P键(大写) + case 0x25: // 左箭头键 + case 0x26: // 上箭头键 + previousAlgorithm(); + break; + + case 'a': // A键 + case 'A': // A键(大写) + autoDemo(); + break; + } + return true; + } + + void handleKeyMsg() + { + while (kbhit()) { + char ch = getch(); + if (!handleInput(ch)) { + break; + } + } + } + + /** + * @brief 运行主循环 + */ + void run() + { + showInterface(); + + while (is_run()) { + handleKeyMsg(); + showInterface(); + } + } + +private: + MyArray m_array; + std::vector m_algorithms; + + int m_currentAlgorithm; + bool m_sorting; +}; + +// 实现 MyArray::notifyVisualization() 方法 +void MyArray::notifyVisualization() +{ + if (m_visualizerPtr) { + m_visualizerPtr->updateScene(); + } +} + +// 实现 MyElement::notifyVisualization() 方法 +void MyElement::notifyVisualization() const +{ + if (m_visualizerPtr && m_enableVisualization) { + m_visualizerPtr->updateScene(); + } +} + +int main() +{ + initgraph(WINDOW_WIDTH, WINDOW_HEIGHT, INIT_RENDERMANUAL); + setbkcolor(BLACK); + setbkmode(TRANSPARENT); + setcaption("EGE - " TEXT_WINDOW_TITLE); + + // 创建可视化状态管理器 + VisualizationState visualizer; + + // 添加所有排序算法 + visualizer.addAlgorithm(TEXT_BUBBLE_SORT, bubbleSort); + visualizer.addAlgorithm(TEXT_SELECTION_SORT, selectionSort); + visualizer.addAlgorithm(TEXT_INSERTION_SORT, insertionSort); + visualizer.addAlgorithm(TEXT_QUICK_SORT, quickSort); + visualizer.addAlgorithm(TEXT_MERGE_SORT, mergeSort); + visualizer.addAlgorithm(TEXT_HEAP_SORT, heapSort); + visualizer.addAlgorithm(TEXT_SHELL_SORT, shellSort); + visualizer.addAlgorithm(TEXT_RADIX_SORT, radixSort); + visualizer.addAlgorithm(TEXT_COUNTING_SORT, countingSort); + + visualizer.addAlgorithm( + TEXT_STD_SORT, [](MyArray::MyIterator first, MyArray::MyIterator last) { std::sort(first, last); }); + visualizer.addAlgorithm(TEXT_STD_STABLE_SORT, + [](MyArray::MyIterator first, MyArray::MyIterator last) { std::stable_sort(first, last); }); + + // 运行可视化演示 + visualizer.run(); + + closegraph(); + return 0; +} diff --git a/cmake_template/ege_demos/graph_star.cpp b/cmake_template/ege_demos/graph_star.cpp new file mode 100644 index 0000000..be3a170 --- /dev/null +++ b/cmake_template/ege_demos/graph_star.cpp @@ -0,0 +1,126 @@ +// 星空屏保程序,请生成为scr后缀,或者手工改后缀,不要直接运行exe +#include "graphics.h" +#include +#include + +#define MAXSTAR 2000 // 星星总数 +int sc_width, sc_heigh; // 记录窗口宽高 +int g_max; + +struct STAR { + double x; + int y; + double step; + int color; +} star[MAXSTAR]; + +// 初始化星星 +void InitStar( int i ) { + double speed = 0.006; + star[i].x = 0; + star[i].y = random( sc_heigh ); + star[i].step = randomf() * speed * 0.9 + speed * 0.1; + star[i].color = ( int )( star[i].step * 255 / speed + 0.5 ); // 速度越快,颜色越亮 + if ( star[i].color > 255 ) { + star[i].color = 255; + } + star[i].color = EGERGB( star[i].color, star[i].color, star[i].color ); +} + +// 移动星星 +void MoveStar( int i, double dt ) { + // 擦掉原来的星星 + putpixel( ( int )( star[i].x * sc_width ), star[i].y, 0 ); + // 计算新位置 + star[i].x += star[i].step * dt * 60; + if ( star[i].x > 1 ) InitStar( i ); + // 画新星星 + putpixel( ( int )( star[i].x * sc_width ), star[i].y, star[i].color ); +} + +int preinit( int argc, char* argv[] ) { + setinitmode( INIT_NOBORDER | INIT_TOPMOST ); // 指定初始化为无边框顶层窗口,并且窗口左上角坐标为(0, 0) + g_max = MAXSTAR; + if ( argc < 2 ) { + //MessageBoxA( NULL, "本屏幕保护程序无配置", "星空屏保", MB_OK ); + //return -1; + } else if ( stricmp( argv[1], "/p" ) == 0 ) { // 小窗口预览模式 + HWND hwnd; + if (strstr(argv[2], "0x") == argv[2]) { + // 十六进制字符串转换为指针 + sscanf( argv[2], "%p", &hwnd ); + } else { + hwnd = (HWND)atoll( argv[2] ); + } + + attachHWND( hwnd ); // 新ege函数 + setinitmode( INIT_NOBORDER | INIT_CHILD | INIT_WITHLOGO ); // 指定初始化为无边框子窗口 + g_max = 200; + return 1; + } else if ( stricmp( argv[1], "/s" ) ) { // 非测试运行模式 + MessageBoxA( NULL, "本屏幕保护程序无配置", "星空屏保", MB_OK ); + return -1; + } + return 0; // 全屏模式 +} + +// 主函数 +int main( int argc, char* argv[] ) { + int i, ms_x = -1024, ms_y = -1024, exitflag = 0; + int fps = 60; + double dtime; + + int mode = preinit( argc, argv ); // 记录初始化模式 + if ( mode < 0 ) return 0; + + randomize(); // 初始化随机种子 + initgraph( -1, -1 ); // 打开图形窗口,以全屏模式 + + showmouse( mode ); + sc_width = getwidth(); + sc_heigh = getheight(); + + // 初始化所有星星 + for ( i = 0; i < g_max; i++ ) { + InitStar( i ); + star[i].x = randomf(); + } + // 绘制星空,按任意键或移动鼠标退出 + setfont( 12, 6, "宋体" ); + setrendermode( RENDER_MANUAL ); + dtime = fclock(); + while ( kbmsg() ) getkey(); + + for ( ; !exitflag && is_run() && kbmsg() == 0; delay_fps( fps ) ) { //每秒画120帧,kbhit(1)是获取键盘任意键的消息,详见pdf + // 如果有鼠标消息 + while ( mousemsg() ) { + mouse_msg msg = getmouse(); + if ( ms_x <= -1024 ) { + ms_x = msg.x; + ms_y = msg.y; + } + // 处理鼠标,移动超出范围就退出 + if ( mode == 0 ) { // 仅全屏模式才处理鼠标 + int x = msg.x, y = msg.y; + x -= ms_x; + y -= ms_y; + if ( x * x + y * y > 400 ) exitflag = 1; + } + } + // 显示星星 + double dt = 1.0 / fps; //fclock() - dtime; + dtime += dt; + for ( int i = 0; i < g_max; i++ ) { + MoveStar( i, dt ); + } + // 显示FPS + { + char str[60]; + sprintf( str, "%8.2f FPS", getfps()); + outtextxy( 0, 0, str ); //显示fps + } + } + closegraph(); // 关闭图形窗口 + return 0; +} + diff --git a/cmake_template/ege_demos/graph_triangle.cpp b/cmake_template/ege_demos/graph_triangle.cpp new file mode 100644 index 0000000..75100fa --- /dev/null +++ b/cmake_template/ege_demos/graph_triangle.cpp @@ -0,0 +1,169 @@ +//彩色渐变三角形动画演示 +#include +#include +#include +#include +#include + +int width = 640, height = 480; + +struct point //定义点,包含坐标,速度 +{ + double x; + double y; + double dx; + double dy; + color_t color; //颜色 + color_t nextcolor, prevcolor; //上一次的颜色,目标颜色 + int chtime, nowtime; //过渡变化时间,当前时间 + int nextcolortime; //距离一下次改变颜色的时间 +}; + +struct poly //定义多边形,包含点的个数,和点数组 +{ + int n_point; + point p[20]; +}; + +struct polys //定义多边形队列组 +{ + int n_poly; //多边形队列长度 + poly p[100]; //多边形数组 +}; + +double rand_float(double dv, double db) //返回一个db 到 db+dv之间的随机浮点数 +{ + return randomf()*dv + db; +} + +color_t getcolor(color_t prevcolor, color_t nextcolor, double t) +{ + if (t <= 0) return prevcolor; + if (t >= 1) return nextcolor; + color_t r, g, b; + r = (color_t)(EGEGET_R(prevcolor) * (1 - t) + EGEGET_R(nextcolor) * t); + g = (color_t)(EGEGET_G(prevcolor) * (1 - t) + EGEGET_G(nextcolor) * t); + b = (color_t)(EGEGET_B(prevcolor) * (1 - t) + EGEGET_B(nextcolor) * t); + if (r > 255) r = 255; + if (g > 255) g = 255; + if (b > 255) b = 255; + return EGERGB(r, g, b); +} + +void movepoint(struct point* b) //根据点的速度属性移动这个点,如果移出屏幕则进行反弹计算 +{ + double dv = 1.0, db = 0.5; + double tw = width / 640.0, th = height / 480.0; + if (b->x <0) b->dx = rand_float(dv, db) * tw; + if (b->y <0) b->dy = rand_float(dv, db) * th; + if (b->x >width) b->dx = -rand_float(dv, db) * tw; + if (b->y >height) b->dy = -rand_float(dv, db) * th; + b->x += b->dx; + b->y += b->dy; + + b->nowtime += 1; + if (b->nowtime > b->chtime + b->nextcolortime) + { + b->nowtime = 0; + b->prevcolor = b->nextcolor; + b->nextcolor = hsv2rgb((float)random(360), 1.0f, 1.0f); + b->chtime = random(1024) + 512; + b->nextcolortime = random(1024) + 512; + } + b->color = getcolor(b->prevcolor, b->nextcolor, (double)b->nowtime / b->chtime); +} + +void movepoly(struct poly* p) //移动单个多边形,内部调用点的移动 +{ + int i; + for (i=0; in_point; ++i) + { + movepoint(&(p->p[i])); + } +} + +void initpolys(struct polys* p, int npoly, int npoint) //初始化多边形队列组 +{ + int i,j; + p->n_poly = npoly; + j = 0; + p->p[j].n_point = npoint; + for (i=0; ip[j].p[i].x = random(width); + p->p[j].p[i].y = random(height); + p->p[j].p[i].dx = (randomf() * 2 + 1); + p->p[j].p[i].dy = (randomf() * 2 + 1); + p->p[j].p[i].color = 0; + p->p[j].p[i].prevcolor = 0; + p->p[j].p[i].nextcolor = hsv2rgb((float)random(360), 1.0f, 0.5f); + p->p[j].p[i].chtime = 1000; + p->p[j].p[i].nowtime = 0; + p->p[j].p[i].nextcolortime = 1000; + } + for (j=1; jp[j] = p->p[j-1]; + } +} + +void draw_poly(struct poly* p) //绘制一个多边形 +{ + ege_colpoint points[100]; + int i; + for (i=0; in_point; ++i) + { + points[i].x = p->p[i].x; + points[i].y = p->p[i].y; + points[i].color = p->p[i].color; + } + points[i].x = p->p[0].x; + points[i].y = p->p[0].y; + points[i].color = p->p[0].color; + //setcolor(color); + fillpoly_gradient(p->n_point, points); +} + +int main() +{ + static struct polys p[10] = {{0}}; + int n_points[10] = {3,3,3,6,7}; + int n_poly[10] = {1,1,1,1,1}; + int n_polys = 3, i; + randomize(); + //图形初始化 + { + setinitmode(INIT_ANIMATION); + initgraph(width, height); + width = getmaxx(); + height = getmaxy(); + setrendermode(RENDER_MANUAL); + } + //多边形对象初始化 + for (i=0; i< n_polys; ++i) + { + initpolys(&p[i], n_poly[i], n_points[i]); + } + fps ui_fps; + //主循环 + for ( ; is_run(); delay_fps(60)) + { + if (kbhit() > 0) //有按键按下就退出 + { + break; + } + for (i=0; i< n_polys; ++i) + { + movepoly(p[i].p); + } + cleardevice(); + for (i=0; i< n_polys; ++i) + { + draw_poly(p[i].p); + } + //imagefilter_blurring(NULL, 0xff, 0x100); + } + closegraph(); + return 0; +} + diff --git a/cmake_template/ege_demos/graph_wave_net.cpp b/cmake_template/ege_demos/graph_wave_net.cpp new file mode 100644 index 0000000..8a1a254 --- /dev/null +++ b/cmake_template/ege_demos/graph_wave_net.cpp @@ -0,0 +1,311 @@ +//碧波荡漾(鼠标拖动弹力物理模拟演示) +#include +#include + +#include +#include +#include + +#define NET_W 256 +#define NET_H 256 + +int g_width = 800; +int g_height = 600; +int g_mod_show = 3; + +double g_d_friction = 0.001; +double g_d_min_a = 0.001; +double g_k = 0.03; /* 劲度系数,不能大于等于0.5 */ + +struct vector_t { + double dx; + double dy; +}; + +struct point_t { + double x; + double y; + double vx; + double vy; + double ax; + double ay; + struct vector_t vt[2]; +}; + +struct net { + struct point_t pt[2][NET_H][NET_W]; + int layer; + int w; + int h; + double dtw; + double dth; + double dmw; + double dmh; +}; + +double s_sqrt(double d) { + if (d<0) { + return -sqrt(-d); + } + return sqrt(d); +} + +double s_pow(double a, double p) { + if (fabs(a)<1) { + return 0; + }else if (a<0) { + return -pow(-a, p); + } + return pow(a, p); +} + +double s_minus(double a, double b) { + if (b>=0) { + if (a >= b) { + return a - b; + } else if (a <= -b) { + return a + b; + } else { + return 0; + } + } else { + if (a>=0) { + return a - b; + } else { + return a + b; + } + } +} + +struct vector_t get_power (struct point_t *p, double x, double y) { /*, double minx=0, double miny=0*/ + struct vector_t vt; + vt.dx = x - p->x; + vt.dy = y - p->y; + //vt.dx = s_minus(vt.dx, minx); + //vt.dy = s_minus(vt.dy, miny); + vt.dx *= g_k; + vt.dy *= g_k; + //vt.dx *= fabs(vt.dx); + //vt.dy *= fabs(vt.dy); + return vt; +} + +void move_point(struct net* pnet, int x, int y) { + double ax = 0; + double ay = 0; + struct vector_t vt; + int i; + int l = pnet->layer; + int dxy[8][2] = { + { 1, 0}, + { 0, 1}, + {-1, 0}, + { 0,-1}, + }; + struct point_t *op = &(pnet->pt[l][y][x]); + + pnet->pt[l^1][y][x] = *op; + for (i=0; i<2; ++i) { + struct point_t *p = &pnet->pt[l][y+dxy[i][1]][x+dxy[i][0]]; + vt = get_power( op, p->x - dxy[i][0]*pnet->dtw, p->y - dxy[i][1]*pnet->dth); + //vt = get_power( op, p->x, p->y, pnet->dtw, pnet->dth); + p->vt[i] = vt; + ax += vt.dx; + ay += vt.dy; + } + ax -= op->vt[0].dx; + ay -= op->vt[0].dy; + ax -= op->vt[1].dx; + ay -= op->vt[1].dy; + + op = &(pnet->pt[l^1][y][x]); + op->ax = ax; + op->ay = ay; + + op->vx += op->ax; + op->vy += op->ay; + + op->x += op->vx; + op->y += op->vy; + + op->vx *= 1 - g_d_friction; + op->vy *= 1 - g_d_friction; + //op->vx = s_minus(op->vx, g_d_friction); + //op->vy = s_minus(op->vy, g_d_friction); +} + +void move_net(struct net* pnet) { + int x, y = 0, l = pnet->layer, i = 1; + struct vector_t vt; + for (x=1; x<=pnet->w; ++x) { + struct point_t *p = &pnet->pt[l][y+1][x+0]; + struct point_t *op = &(pnet->pt[l][y][x]); + vt = get_power( op, p->x, p->y - pnet->dth); + //vt = get_power( op, p->x, p->y, pnet->dtw, pnet->dth); + p->vt[i] = vt; + } + i = 0; + for (y=1; y<=pnet->h; ++y) { + x = 0; + { + struct point_t *p = &pnet->pt[l][y][x+1]; + struct point_t *op = &(pnet->pt[l][y][x]); + vt = get_power( op, p->x - pnet->dtw, p->y); + //vt = get_power( op, p->x, p->y, pnet->dtw, pnet->dth); + p->vt[i] = vt; + for (x=1; x<=pnet->w; ++x) { + move_point(pnet, x, y); + } + } + } + pnet->layer ^= 1; +} + +void init_net(struct net* pnet, int w, int h, int sw, int sh) { + double dtw = sw / (double)(w - 1); + double dth = sh / (double)(h - 1); + int x, y, l; + struct point_t pt = {0}; + + pnet->layer = 0; + pnet->w = w; + pnet->h = h; + pnet->dtw = dtw; + pnet->dth = dth; + + for (y=0; y<=h+1; ++y) { + for (x=0; x<=w+1; ++x) { + for (l=0; l<2; ++l) { + pnet->pt[l][y][x] = pt; + pnet->pt[l][y][x].x = (dtw*(x-1)); + pnet->pt[l][y][x].y = (dth*(y-1)); + } + } + } +} + +void draw_net(struct net* pnet) { + int x, y, l = pnet->layer; + POINT pt[NET_W + NET_H]; + + for (y=0; y<=pnet->h; ++y) { + //if (y % g_mod_show == 0) + { + for (x=0; x<=pnet->w + 1; ++x) { + pt[x].x = (int)(pnet->pt[l][y][x].x + 0.5); + pt[x].y = (int)(pnet->pt[l][y][x].y + 0.5); + //line_f(pnet->pt[l][y][x].x, pnet->pt[l][y][x].y, pnet->pt[l][y][x+1].x, pnet->pt[l][y][x+1].y); + } + drawbezier(pnet->w + 2, (int*)pt); + } + } + for (x=0; x<=pnet->w; ++x) { + //if (x % g_mod_show == 0) + { + for (y=0; y<=pnet->h + 1; ++y) { + pt[y].x = (int)(pnet->pt[l][y][x].x + 0.5); + pt[y].y = (int)(pnet->pt[l][y][x].y + 0.5); + //line_f(pnet->pt[l][y][x].x, pnet->pt[l][y][x].y, pnet->pt[l][y+1][x].x, pnet->pt[l][y+1][x].y); + } + drawbezier(pnet->h + 2, (int*)pt); + } + } +} + +void cap_pt(struct net* pnet, int px, int py, int op) { + static int cp_x, cp_y, b_cp = 0; + + if (op) { + int y, x, l=pnet->layer; + if (b_cp == 0) { + int mx = 1, my = 1; + double mdis = 1e9, dis; + for (y=1; yh; ++y) { + if (y % g_mod_show == 0) + continue; + for (x=1; xw; ++x) { + if (x % g_mod_show == 0) + continue; + dis = fabs(px - pnet->pt[l][y][x].x) + fabs(py - pnet->pt[l][y][x].y); + if (dis < mdis) { + mx = x; + my = y; + mdis = dis; + } + } + } + cp_x = mx; + cp_y = my; + pnet->pt[l][cp_y][cp_x].x = px; + pnet->pt[l][cp_y][cp_x].y = py; + b_cp = 1; + } else { + pnet->pt[l][cp_y][cp_x].x = px; + pnet->pt[l][cp_y][cp_x].y = py; + } + pnet->pt[l][cp_y][cp_x].vx = 0; + pnet->pt[l][cp_y][cp_x].vy = 0; + } else { + b_cp = 0; + } +} + +void init() { + int g = TRUECOLORSIZE, m = (g_height<<16) | g_width; + //initgraph(&g, &m, "碧波荡漾"); + //setinitmode(3); + initgraph(640, 480); + g_width = getwidth(); + g_height = getheight(); +} + +void getmouse(int *x, int *y, int *key) { + mousepos(x, y); + *key = keystate(key_mouse_l); +} + +struct net g_net; + +int main(int argc, char* argv[]) { + char str[100] = ""; + int basepoint = 20; + /* + if (argc < 2) return 0; + if (stricmp(argv[1], "/p") == 0) + { + HWND hwnd; + sscanf(argv[2], "%d", &hwnd); + attachHWND(hwnd); + } + else if (stricmp(argv[1], "/s")) return 0;//*/ + + init(); + init_net(&g_net, basepoint * 4, basepoint * 3, g_width, g_height); + setbkmode(TRANSPARENT); + + fps fps_obj; + setrendermode(RENDER_MANUAL); + for (; kbhit() == 0; delay_fps(60)) { + cleardevice(); + setcolor(0x8000); + draw_net(&g_net); + + setcolor(0xFFFFFF); + outtextxy(0,0,str); + + move_net(&g_net); + move_net(&g_net); + { + int x, y, k; + getmouse(&x, &y, &k); + if (k) { + cap_pt(&g_net, x, y, 1); + } else { + cap_pt(&g_net, x, y, 0); + } + } + } + closegraph(); + return 0; +} + diff --git a/src/demoOptions.ts b/src/demoOptions.ts new file mode 100644 index 0000000..542d8a4 --- /dev/null +++ b/src/demoOptions.ts @@ -0,0 +1,334 @@ +/** + * Author: wysaid + * Date: 2025-12-16 + * Demo options management for EGE VSCode Extension + */ + +import * as path from 'path'; +import * as fs from 'fs-extra'; + +/** + * Demo 分类枚举 + */ +export enum DemoCategory { + BASIC = 'BASIC', + GAME = 'GAME', + GRAPHICS = 'GRAPHICS', + ALGORITHM = 'ALGORITHM', + PHYSICS = 'PHYSICS', + FRACTAL = 'FRACTAL', + IMAGE = 'IMAGE', + CAMERA = 'CAMERA' +} + +/** + * Demo category display names + */ +export const DemoCategoryDisplayNames: { [key: string]: { [key in DemoCategory]: string } } = { + 'en': { + [DemoCategory.BASIC]: 'Basic', + [DemoCategory.GAME]: 'Game', + [DemoCategory.GRAPHICS]: 'Graphics', + [DemoCategory.ALGORITHM]: 'Algorithm', + [DemoCategory.PHYSICS]: 'Physics', + [DemoCategory.FRACTAL]: 'Fractal', + [DemoCategory.IMAGE]: 'Image', + [DemoCategory.CAMERA]: 'Camera' + }, + 'zh': { + [DemoCategory.BASIC]: '基础入门', + [DemoCategory.GAME]: '游戏示例', + [DemoCategory.GRAPHICS]: '图形绘制', + [DemoCategory.ALGORITHM]: '算法可视化', + [DemoCategory.PHYSICS]: '物理模拟', + [DemoCategory.FRACTAL]: '分形与数学', + [DemoCategory.IMAGE]: '图像处理', + [DemoCategory.CAMERA]: '摄像头' + } +}; + +/** + * Demo 信息数据类 + */ +export interface DemoInfo { + title: string; + description: string; + category: DemoCategory; +} + +/** + * Demo 选项数据类 + */ +export interface DemoOption { + displayName: string; + fileName: string | null; + info: DemoInfo | null; +} + +/** + * Demo 元数据注册表 + * 包含所有 Demo 的中文和英文标题和描述 + */ +class DemoMetadataRegistry { + private metadata: { [key: string]: { zh: DemoInfo, en: DemoInfo } } = { + // 基础入门 + 'main.cpp': { + zh: { title: 'Hello World', description: '最简单的EGE程序,初始化窗口并显示', category: DemoCategory.BASIC }, + en: { title: 'Hello World', description: 'The simplest EGE program, initialize window and display', category: DemoCategory.BASIC } + }, + + // 摄像头 + 'camera_base.cpp': { + zh: { title: '摄像头基础', description: '调用系统摄像头,支持切换设备和分辨率', category: DemoCategory.CAMERA }, + en: { title: 'Camera Basic', description: 'Call system camera, support device and resolution switching', category: DemoCategory.CAMERA } + }, + 'camera_wave.cpp': { + zh: { title: '摄像头水波特效', description: '实时摄像头画面配合水波网格变形效果', category: DemoCategory.CAMERA }, + en: { title: 'Camera Wave Effect', description: 'Real-time camera feed with water wave mesh deformation effect', category: DemoCategory.CAMERA } + }, + + // 游戏示例 + 'game_gomoku.cpp': { + zh: { title: '五子棋', description: '经典五子棋游戏,带简单AI对手和音效', category: DemoCategory.GAME }, + en: { title: 'Gomoku', description: 'Classic Gomoku game with simple AI opponent and sound effects', category: DemoCategory.GAME } + }, + 'game_snake.cpp': { + zh: { title: '贪吃蛇', description: '约90行代码实现的精简版贪吃蛇游戏', category: DemoCategory.GAME }, + en: { title: 'Snake Game', description: 'Simplified snake game implemented in about 90 lines of code', category: DemoCategory.GAME } + }, + 'game_tetris.cpp': { + zh: { title: '俄罗斯方块', description: '完整的俄罗斯方块游戏实现', category: DemoCategory.GAME }, + en: { title: 'Tetris', description: 'Complete Tetris game implementation', category: DemoCategory.GAME } + }, + 'game_type.cpp': { + zh: { title: '打字练习', description: '字母下落式打字练习小游戏', category: DemoCategory.GAME }, + en: { title: 'Typing Practice', description: 'Letter-falling typing practice mini game', category: DemoCategory.GAME } + }, + + // 图形绘制 + 'graph_5star.cpp': { + zh: { title: '五角星旋转', description: '绘制五角星并展示旋转动画效果', category: DemoCategory.GRAPHICS }, + en: { title: '5-Star Rotation', description: 'Draw a pentagram and demonstrate rotation animation', category: DemoCategory.GRAPHICS } + }, + 'graph_alpha.cpp': { + zh: { title: 'Alpha透明度', description: '演示Alpha通道透明度与图层混合', category: DemoCategory.GRAPHICS }, + en: { title: 'Alpha Transparency', description: 'Demonstrate alpha channel transparency and layer blending', category: DemoCategory.GRAPHICS } + }, + 'graph_arrow.cpp': { + zh: { title: '箭头绘制', description: '多种箭头绘制算法演示', category: DemoCategory.GRAPHICS }, + en: { title: 'Arrow Drawing', description: 'Demonstration of various arrow drawing algorithms', category: DemoCategory.GRAPHICS } + }, + 'graph_clock.cpp': { + zh: { title: '模拟时钟', description: '绘制带时针分针秒针的模拟时钟', category: DemoCategory.GRAPHICS }, + en: { title: 'Analog Clock', description: 'Draw analog clock with hour, minute and second hands', category: DemoCategory.GRAPHICS } + }, + 'graph_getimage.cpp': { + zh: { title: '图片加载', description: '演示如何加载显示PNG/JPG图片', category: DemoCategory.IMAGE }, + en: { title: 'Image Loading', description: 'Demonstrate how to load and display PNG/JPG images', category: DemoCategory.IMAGE } + }, + 'graph_lines.cpp': { + zh: { title: '变幻线', description: '多边形变幻线屏保特效', category: DemoCategory.GRAPHICS }, + en: { title: 'Morphing Lines', description: 'Polygon morphing lines screensaver effect', category: DemoCategory.GRAPHICS } + }, + 'graph_new_drawimage.cpp': { + zh: { title: '图像变换', description: 'PIMAGE图像绘制与矩阵变换', category: DemoCategory.IMAGE }, + en: { title: 'Image Transform', description: 'PIMAGE image drawing and matrix transformation', category: DemoCategory.IMAGE } + }, + 'graph_rotateimage.cpp': { + zh: { title: '图片旋转', description: '图片旋转缩放动画演示', category: DemoCategory.IMAGE }, + en: { title: 'Image Rotation', description: 'Image rotation and scaling animation demonstration', category: DemoCategory.IMAGE } + }, + 'graph_rotatetransparent.cpp': { + zh: { title: '透明旋转', description: '带透明背景的图片旋转', category: DemoCategory.IMAGE }, + en: { title: 'Transparent Rotation', description: 'Image rotation with transparent background', category: DemoCategory.IMAGE } + }, + 'graph_star.cpp': { + zh: { title: '星空屏保', description: '满天繁星流动的屏保效果', category: DemoCategory.GRAPHICS }, + en: { title: 'Starfield Screensaver', description: 'Flowing starfield screensaver effect', category: DemoCategory.GRAPHICS } + }, + 'graph_triangle.cpp': { + zh: { title: '渐变三角形', description: '彩色渐变填充三角形动画', category: DemoCategory.GRAPHICS }, + en: { title: 'Gradient Triangle', description: 'Colorful gradient-filled triangle animation', category: DemoCategory.GRAPHICS } + }, + + // 算法可视化 + 'graph_astar_pathfinding.cpp': { + zh: { title: 'A*寻路算法', description: 'A*路径搜索算法的可视化演示', category: DemoCategory.ALGORITHM }, + en: { title: 'A* Pathfinding', description: 'Visualization of A* path search algorithm', category: DemoCategory.ALGORITHM } + }, + 'graph_sort_visualization.cpp': { + zh: { title: '排序算法可视化', description: '11种排序算法的动态可视化对比', category: DemoCategory.ALGORITHM }, + en: { title: 'Sorting Visualization', description: 'Dynamic visualization comparison of 11 sorting algorithms', category: DemoCategory.ALGORITHM } + }, + 'graph_kmeans.cpp': { + zh: { title: 'K-Means聚类', description: 'K-Means机器学习算法可视化', category: DemoCategory.ALGORITHM }, + en: { title: 'K-Means Clustering', description: 'Visualization of K-Means machine learning algorithm', category: DemoCategory.ALGORITHM } + }, + 'graph_game_of_life.cpp': { + zh: { title: '生命游戏', description: '康威生命游戏元胞自动机模拟', category: DemoCategory.ALGORITHM }, + en: { title: 'Game of Life', description: 'Conway\'s Game of Life cellular automaton simulation', category: DemoCategory.ALGORITHM } + }, + 'graph_function_visualization.cpp': { + zh: { title: '函数图像', description: '基于蒙特卡洛法的2D数学函数绘制', category: DemoCategory.FRACTAL }, + en: { title: 'Function Visualization', description: '2D mathematical function drawing based on Monte Carlo method', category: DemoCategory.FRACTAL } + }, + + // 物理模拟 + 'graph_ball.cpp': { + zh: { title: '弹球碰撞', description: '多彩弹球物理碰撞模拟', category: DemoCategory.PHYSICS }, + en: { title: 'Ball Collision', description: 'Colorful ball physics collision simulation', category: DemoCategory.PHYSICS } + }, + 'graph_boids.cpp': { + zh: { title: '群集模拟', description: 'Boids算法模拟鸟群/鱼群行为', category: DemoCategory.PHYSICS }, + en: { title: 'Boids Simulation', description: 'Boids algorithm simulating flock/school behavior', category: DemoCategory.PHYSICS } + }, + 'graph_mouseball.cpp': { + zh: { title: '鼠标拖动弹球', description: '用鼠标拖动弹球的物理模拟', category: DemoCategory.PHYSICS }, + en: { title: 'Mouse Drag Ball', description: 'Physics simulation of dragging balls with mouse', category: DemoCategory.PHYSICS } + }, + 'graph_wave_net.cpp': { + zh: { title: '碧波荡漾', description: '鼠标拖动弹力网格物理模拟', category: DemoCategory.PHYSICS }, + en: { title: 'Rippling Waves', description: 'Physics simulation of elastic mesh dragged by mouse', category: DemoCategory.PHYSICS } + }, + + // 分形与数学 + 'graph_julia.cpp': { + zh: { title: 'Julia集', description: 'Julia分形集屏保动画', category: DemoCategory.FRACTAL }, + en: { title: 'Julia Set', description: 'Julia fractal set screensaver animation', category: DemoCategory.FRACTAL } + }, + 'graph_mandelbrot.cpp': { + zh: { title: 'Mandelbrot集', description: '鼠标缩放Mandelbrot分形集', category: DemoCategory.FRACTAL }, + en: { title: 'Mandelbrot Set', description: 'Mouse zoom Mandelbrot fractal set', category: DemoCategory.FRACTAL } + }, + 'graph_catharine.cpp': { + zh: { title: '烟花特效', description: '绚丽的烟花粒子效果', category: DemoCategory.GRAPHICS }, + en: { title: 'Fireworks Effect', description: 'Gorgeous fireworks particle effect', category: DemoCategory.GRAPHICS } + } + }; + + /** + * 获取指定文件的元数据 + */ + getInfo(fileName: string | null, language: string = 'zh'): DemoInfo | null { + if (fileName === null) { + fileName = 'main.cpp'; + } + const meta = this.metadata[fileName]; + if (!meta) { + return null; + } + return language === 'zh' ? meta.zh : meta.en; + } + + /** + * 获取默认的 Hello World 信息 + */ + getDefaultInfo(language: string = 'zh'): DemoInfo { + return this.getInfo('main.cpp', language)!; + } +} + +/** + * Demo 选项管理器 + */ +export class DemoOptionsManager { + private static registry = new DemoMetadataRegistry(); + private static demosDir: string = ''; + + /** + * 设置 demos 目录路径 + */ + static setDemosDir(dir: string): void { + this.demosDir = dir; + } + + /** + * 获取所有可用的 Demo 选项 + */ + static getDemoOptions(language: string = 'zh'): DemoOption[] { + const options: DemoOption[] = []; + + // 添加默认的 Hello World 选项 + const defaultInfo = this.registry.getDefaultInfo(language); + options.push({ + displayName: defaultInfo.title, + fileName: null, + info: defaultInfo + }); + + // 从 ege_demos 目录动态加载 Demo 文件 + try { + const demoFiles = this.discoverDemoFiles(); + demoFiles.sort().forEach(fileName => { + const info = this.registry.getInfo(fileName, language); + const displayName = info ? info.title : this.generateDisplayName(fileName); + options.push({ + displayName, + fileName, + info + }); + }); + } catch (e) { + console.error('Failed to load demo options', e); + } + + return options; + } + + /** + * 按分类获取 Demo 选项 + */ + static getDemoOptionsByCategory(language: string = 'zh'): Map { + const options = this.getDemoOptions(language); + const categoryMap = new Map(); + + options.forEach(option => { + if (option.info) { + const category = option.info.category; + if (!categoryMap.has(category)) { + categoryMap.set(category, []); + } + categoryMap.get(category)!.push(option); + } + }); + + return categoryMap; + } + + /** + * 从文件名生成默认显示名称(当没有元数据时使用) + */ + private static generateDisplayName(fileName: string): string { + return fileName + .replace('.cpp', '') + .replace(/_/g, ' ') + .split(' ') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + + /** + * 扫描 ege_demos 目录,发现所有 .cpp 文件 + */ + private static discoverDemoFiles(): string[] { + const files: string[] = []; + + if (!this.demosDir || !fs.existsSync(this.demosDir)) { + console.warn('Demos directory not found:', this.demosDir); + return files; + } + + try { + const dirFiles = fs.readdirSync(this.demosDir); + dirFiles.forEach(file => { + if (file.endsWith('.cpp')) { + files.push(file); + } + }); + } catch (e) { + console.error('Failed to discover demo files', e); + } + + return files; + } +} diff --git a/src/extension.ts b/src/extension.ts index 6c4893e..887cc2e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,11 +20,11 @@ function activate(context: vscode.ExtensionContext) { EGEInstaller.registerContext(context); context.subscriptions.push(vscode.commands.registerCommand('ege.setupProject', async () => { - setupProject(); + await setupProject(); })); context.subscriptions.push(vscode.commands.registerCommand('ege.setupProjectWithEgeSrc', async () => { - setupProject(true); + await setupProject(true); })); context.subscriptions.push(vscode.commands.registerCommand('ege.setupGlobal', () => { diff --git a/src/setupProject.ts b/src/setupProject.ts index 65d6f53..9a6edfa 100644 --- a/src/setupProject.ts +++ b/src/setupProject.ts @@ -8,9 +8,22 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import { ege } from './ege'; import { copyDirRecursiveIfNotExist, copyIfNotExist } from './utils'; -import { t } from './i18n'; +import { t, i18n } from './i18n'; +import { DemoOptionsManager, DemoOption, DemoCategory, DemoCategoryDisplayNames } from './demoOptions'; -export function setupProject(usingSource?: boolean) { +export async function setupProject(usingSource?: boolean) { + // Initialize demo options manager with the demos directory path + DemoOptionsManager.setDemosDir(path.join(__dirname, '../cmake_template/ege_demos')); + + // Get current language + const language = i18n.getCurrentLanguage(); + + // Show demo selection dialog + const demoOption = await showDemoSelectionDialog(language); + if (!demoOption) { + // User cancelled + return; + } const workspaceFolders = vscode.workspace.workspaceFolders; for (const workspaceFolder of workspaceFolders || []) { @@ -36,8 +49,24 @@ export function setupProject(usingSource?: boolean) { } if (!hasCppFile) { - /// 拷贝 main.cpp - copyIfNotExist(path.join(__dirname, `../cmake_template/main.cpp`), `${workspaceDir}/main.cpp`); + /// 拷贝选中的 demo 文件 + const sourceFile = demoOption.fileName + ? path.join(__dirname, `../cmake_template/ege_demos/${demoOption.fileName}`) + : path.join(__dirname, `../cmake_template/main.cpp`); + + copyIfNotExist(sourceFile, `${workspaceDir}/main.cpp`); + + // If the demo requires image files, copy them too + if (demoOption.fileName === 'graph_getimage.cpp') { + const demosDir = path.join(__dirname, '../cmake_template/ege_demos'); + const imageFiles = ['getimage.jpg', 'getimage.png']; + imageFiles.forEach(imgFile => { + const imgPath = path.join(demosDir, imgFile); + if (fs.existsSync(imgPath)) { + copyIfNotExist(imgPath, `${workspaceDir}/${imgFile}`); + } + }); + } } else { ege.printInfo(t('message.skipCreateMainCpp')); } @@ -68,4 +97,66 @@ export function setupProject(usingSource?: boolean) { } ege.getOutputChannel().show(); +} + +/** + * Show demo selection dialog + */ +async function showDemoSelectionDialog(language: string): Promise { + const demoOptions = DemoOptionsManager.getDemoOptions(language); + const categoryMap = DemoOptionsManager.getDemoOptionsByCategory(language); + + // Create QuickPick items grouped by category + const items: (vscode.QuickPickItem & { demoOption: DemoOption })[] = []; + + // Sort categories in a meaningful order + const categoryOrder = [ + DemoCategory.BASIC, + DemoCategory.GRAPHICS, + DemoCategory.GAME, + DemoCategory.ALGORITHM, + DemoCategory.PHYSICS, + DemoCategory.FRACTAL, + DemoCategory.IMAGE, + DemoCategory.CAMERA + ]; + + const categoryDisplayNames = DemoCategoryDisplayNames[language] || DemoCategoryDisplayNames['zh']; + + categoryOrder.forEach(category => { + const options = categoryMap.get(category); + if (options && options.length > 0) { + // Add category separator + items.push({ + label: '', + kind: vscode.QuickPickItemKind.Separator, + demoOption: options[0] // Placeholder + } as any); + + items.push({ + label: categoryDisplayNames[category], + kind: vscode.QuickPickItemKind.Separator, + demoOption: options[0] // Placeholder + } as any); + + // Add demos in this category + options.forEach(option => { + items.push({ + label: `$(file-code) ${option.displayName}`, + description: option.info?.description || '', + detail: option.fileName || 'main.cpp', + demoOption: option + }); + }); + } + }); + + // Show QuickPick + const selected = await vscode.window.showQuickPick(items.filter(item => item.kind !== vscode.QuickPickItemKind.Separator), { + placeHolder: language === 'zh' ? '请选择一个示例模板' : 'Please select a demo template', + matchOnDescription: true, + matchOnDetail: true + }); + + return selected?.demoOption; } \ No newline at end of file From 985c529adaaffbd6f47929ef78d3ea6d9323427f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 03:06:09 +0000 Subject: [PATCH 3/9] Update documentation for demo template feature Co-authored-by: wysaid <1430725+wysaid@users.noreply.github.com> --- CHANGELOG.md | 13 ++++++++ README.md | 9 ++++++ README.zh-CN.md | 9 ++++++ src/demoOptions.test.ts | 70 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 src/demoOptions.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d6cb08f..76006cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to the "ege" extension will be documented in this file. Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. +## Unreleased + +- **New Feature**: 新增 31+ 个示例模板选择功能 + - 创建新项目时可以从多种示例模板中选择 + - 包含基础入门、游戏示例、图形绘制、算法可视化、物理模拟、分形与数学、图像处理、摄像头等 8 大类 + - 支持中英文双语界面 + - 示例包括:贪吃蛇、俄罗斯方块、五子棋、A*寻路、排序算法可视化、弹球碰撞、Julia集、Mandelbrot集等 +- **New Feature**: Added 31+ demo template selection + - Choose from a variety of demo templates when creating a new project + - Includes 8 categories: Basic, Games, Graphics, Algorithms, Physics, Fractals, Image Processing, Camera + - Bilingual support (Chinese and English) + - Demos include: Snake, Tetris, Gomoku, A* Pathfinding, Sorting visualization, Ball collision, Julia Set, Mandelbrot Set, and more + ## 1.0.3 - 修正 vscode 项目模板的一些小问题 diff --git a/README.md b/README.md index 403c530..d3c60df 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,15 @@ A VSCode plugin to help the configuration of [ege](https://xege.org). Enjoy it! ## Features - Generate `EGE Projects` with single click +- **31+ Demo Templates**: Choose from a variety of demo templates when creating a new project + - Basic: Hello World + - Games: Snake, Tetris, Gomoku, Typing Practice + - Graphics: Star animations, Clock, Lines effects, Fireworks + - Algorithms: A* Pathfinding, Sorting visualization, K-Means clustering, Game of Life + - Physics: Ball collision, Boids simulation, Wave effects + - Fractals: Julia Set, Mandelbrot Set + - Image Processing: Loading, Rotation, Transformations + - Camera: Camera basics, Wave effects - Build and run single cpp files with EGE support.(`graphics.h`) - Add EGE CMake Project Template for `C/C++` source. - Support MacOS/Linux (By `mingw-w64` and `wine`) diff --git a/README.zh-CN.md b/README.zh-CN.md index ab3435a..adbbca6 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -5,6 +5,15 @@ ## 功能特性 - 一键生成 `EGE 项目` +- **31+ 示例模板**: 创建新项目时可以从多种示例模板中选择 + - 基础入门:Hello World + - 游戏示例:贪吃蛇、俄罗斯方块、五子棋、打字练习 + - 图形绘制:星空动画、时钟、变幻线、烟花特效 + - 算法可视化:A*寻路算法、排序算法可视化、K-Means聚类、生命游戏 + - 物理模拟:弹球碰撞、群集模拟、水波效果 + - 分形与数学:Julia集、Mandelbrot集 + - 图像处理:加载、旋转、变换 + - 摄像头:摄像头基础、水波特效 - 构建并运行支持 EGE 的单个 cpp 文件 (`graphics.h`) - 为 `C/C++` 源代码添加 EGE CMake 项目模板 - 支持 MacOS/Linux (通过 `mingw-w64` 和 `wine`) diff --git a/src/demoOptions.test.ts b/src/demoOptions.test.ts new file mode 100644 index 0000000..cdaedca --- /dev/null +++ b/src/demoOptions.test.ts @@ -0,0 +1,70 @@ +/** + * Test file for demo options functionality + */ + +import * as path from 'path'; +import { DemoOptionsManager, DemoCategory } from './demoOptions'; + +// Test function to verify demo options functionality +export function testDemoOptions() { + console.log('=== Testing demo options functionality ==='); + + // Set the demos directory + const demosDir = path.join(__dirname, '../cmake_template/ege_demos'); + DemoOptionsManager.setDemosDir(demosDir); + + // Test getting demo options in Chinese + console.log('\n--- Testing Chinese demo options ---'); + const zhOptions = DemoOptionsManager.getDemoOptions('zh'); + console.log(`Total demos (Chinese): ${zhOptions.length}`); + console.log('First 5 demos:'); + zhOptions.slice(0, 5).forEach(option => { + console.log(` - ${option.displayName}`); + console.log(` File: ${option.fileName || 'main.cpp'}`); + console.log(` Description: ${option.info?.description || 'N/A'}`); + console.log(` Category: ${option.info?.category || 'N/A'}`); + }); + + // Test getting demo options in English + console.log('\n--- Testing English demo options ---'); + const enOptions = DemoOptionsManager.getDemoOptions('en'); + console.log(`Total demos (English): ${enOptions.length}`); + console.log('First 5 demos:'); + enOptions.slice(0, 5).forEach(option => { + console.log(` - ${option.displayName}`); + console.log(` File: ${option.fileName || 'main.cpp'}`); + console.log(` Description: ${option.info?.description || 'N/A'}`); + console.log(` Category: ${option.info?.category || 'N/A'}`); + }); + + // Test getting demo options by category + console.log('\n--- Testing demos by category ---'); + const categoryMap = DemoOptionsManager.getDemoOptionsByCategory('zh'); + console.log(`Number of categories: ${categoryMap.size}`); + categoryMap.forEach((options, category) => { + console.log(` ${category}: ${options.length} demos`); + }); + + // Test specific categories + console.log('\n--- Testing specific categories ---'); + const gameOptions = categoryMap.get(DemoCategory.GAME); + if (gameOptions) { + console.log(`Game demos (${gameOptions.length}):`); + gameOptions.forEach(option => { + console.log(` - ${option.displayName}: ${option.info?.description}`); + }); + } + + const graphicsOptions = categoryMap.get(DemoCategory.GRAPHICS); + if (graphicsOptions) { + console.log(`Graphics demos (${graphicsOptions.length}):`); + graphicsOptions.forEach(option => { + console.log(` - ${option.displayName}: ${option.info?.description}`); + }); + } + + console.log('=== End of demo options test ==='); +} + +// Export test function for manual testing +export { testDemoOptions as test }; From 823d864cd0922c0526b16991ce93d738bff08e2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 03:08:49 +0000 Subject: [PATCH 4/9] Address code review feedback - improve error handling and type safety Co-authored-by: wysaid <1430725+wysaid@users.noreply.github.com> --- src/demoOptions.ts | 13 ++++++++++++- src/setupProject.ts | 17 +++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/demoOptions.ts b/src/demoOptions.ts index 542d8a4..ab11095 100644 --- a/src/demoOptions.ts +++ b/src/demoOptions.ts @@ -224,7 +224,18 @@ class DemoMetadataRegistry { * 获取默认的 Hello World 信息 */ getDefaultInfo(language: string = 'zh'): DemoInfo { - return this.getInfo('main.cpp', language)!; + const info = this.getInfo('main.cpp', language); + if (!info) { + // Fallback in case main.cpp metadata is missing + return { + title: 'Hello World', + description: language === 'zh' + ? '最简单的EGE程序,初始化窗口并显示' + : 'The simplest EGE program, initialize window and display', + category: DemoCategory.BASIC + }; + } + return info; } } diff --git a/src/setupProject.ts b/src/setupProject.ts index 9a6edfa..bf983be 100644 --- a/src/setupProject.ts +++ b/src/setupProject.ts @@ -126,17 +126,18 @@ async function showDemoSelectionDialog(language: string): Promise { const options = categoryMap.get(category); if (options && options.length > 0) { - // Add category separator - items.push({ - label: '', - kind: vscode.QuickPickItemKind.Separator, - demoOption: options[0] // Placeholder - } as any); + // Add empty separator between categories + if (items.length > 0) { + items.push({ + label: '', + kind: vscode.QuickPickItemKind.Separator + } as any); + } + // Add category label items.push({ label: categoryDisplayNames[category], - kind: vscode.QuickPickItemKind.Separator, - demoOption: options[0] // Placeholder + kind: vscode.QuickPickItemKind.Separator } as any); // Add demos in this category From 51aef241ce83d8993299982f0b4c6c20cf0f56ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 03:10:45 +0000 Subject: [PATCH 5/9] Feature complete: Demo template selection with 31+ templates Co-authored-by: wysaid <1430725+wysaid@users.noreply.github.com> --- _codeql_detected_source_root | 1 + 1 file changed, 1 insertion(+) create mode 120000 _codeql_detected_source_root diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file From 13a85f7dd1159c69654d2c5d4ed1ee851d4b3476 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 07:57:02 +0000 Subject: [PATCH 6/9] Add file content comparison and user confirmation for template creation Co-authored-by: wysaid <1430725+wysaid@users.noreply.github.com> --- src/i18n.ts | 36 ++++++++++ src/setupProject.ts | 133 +++++++++++++++++++++---------------- src/utils.ts | 157 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+), 56 deletions(-) diff --git a/src/i18n.ts b/src/i18n.ts index 8f5b699..78db229 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -46,6 +46,14 @@ export interface LanguageStrings { 'message.noContentInInstallDir': string; 'message.multiInstallDirFound': string; 'message.cleanupCacheDone': string; + 'message.fileExists': string; + 'message.fileContentDifferent': string; + 'message.fileContentSame': string; + 'message.directoryExists': string; + + // Prompt messages + 'prompt.overwriteFile': string; + 'prompt.replaceDirectory': string; // Configuration descriptions 'config.updateUrl.description': string; @@ -56,6 +64,10 @@ export interface LanguageStrings { 'button.ok': string; 'button.cancel': string; 'button.retry': string; + 'button.yes': string; + 'button.no': string; + 'button.yesToAll': string; + 'button.noToAll': string; // Titles 'title.chooseCompiler': string; @@ -102,6 +114,14 @@ const englishStrings: LanguageStrings = { 'message.noContentInInstallDir': 'No content in the installation dir at: {0}', 'message.multiInstallDirFound': 'Multi installation dir found, pick the first: {0}', 'message.cleanupCacheDone': 'Cleanup ege plugin cache - Done!', + 'message.fileExists': 'File {0} already exists', + 'message.fileContentDifferent': 'File {0} content is different from template', + 'message.fileContentSame': 'File {0} content is identical to template, skipped', + 'message.directoryExists': 'Directory {0} already exists', + + // Prompt messages + 'prompt.overwriteFile': 'File {0} already exists and content is different. Overwrite?', + 'prompt.replaceDirectory': 'Directory {0} already exists. Replace entire directory?', // Configuration descriptions 'config.updateUrl.description': 'An url to get latest version of EGE (default: https://xege.org/download/ege-latest-version)', @@ -112,6 +132,10 @@ const englishStrings: LanguageStrings = { 'button.ok': 'OK', 'button.cancel': 'Cancel', 'button.retry': 'Retry', + 'button.yes': 'Yes', + 'button.no': 'No', + 'button.yesToAll': 'Yes to All', + 'button.noToAll': 'No to All', // Titles 'title.chooseCompiler': 'EGE: Choose the specific compiler to install.', @@ -158,6 +182,14 @@ const chineseStrings: LanguageStrings = { 'message.noContentInInstallDir': '在安装目录中没有内容:{0}', 'message.multiInstallDirFound': '找到多个安装目录,选择第一个:{0}', 'message.cleanupCacheDone': '清理 ege 插件缓存 - 完成!', + 'message.fileExists': '文件 {0} 已存在', + 'message.fileContentDifferent': '文件 {0} 的内容与模板不同', + 'message.fileContentSame': '文件 {0} 内容与模板相同,已跳过', + 'message.directoryExists': '目录 {0} 已存在', + + // Prompt messages + 'prompt.overwriteFile': '文件 {0} 已存在且内容不同,是否覆盖?', + 'prompt.replaceDirectory': '目录 {0} 已存在,是否替换整个目录?', // Configuration descriptions 'config.updateUrl.description': '获取最新版本 EGE 的 URL(默认:https://xege.org/download/ege-latest-version)', @@ -168,6 +200,10 @@ const chineseStrings: LanguageStrings = { 'button.ok': '确定', 'button.cancel': '取消', 'button.retry': '重试', + 'button.yes': '是', + 'button.no': '否', + 'button.yesToAll': '全部是', + 'button.noToAll': '全部否', // Titles 'title.chooseCompiler': 'EGE: 选择要安装的特定编译器。', diff --git a/src/setupProject.ts b/src/setupProject.ts index bf983be..35da9de 100644 --- a/src/setupProject.ts +++ b/src/setupProject.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs-extra'; import * as path from 'path'; import { ege } from './ege'; -import { copyDirRecursiveIfNotExist, copyIfNotExist } from './utils'; +import { copyDirRecursiveIfNotExist, copyIfNotExist, copyFileWithPrompt, copyDirRecursiveWithPrompt, replaceDirWithPrompt } from './utils'; import { t, i18n } from './i18n'; import { DemoOptionsManager, DemoOption, DemoCategory, DemoCategoryDisplayNames } from './demoOptions'; @@ -27,72 +27,93 @@ export async function setupProject(usingSource?: boolean) { const workspaceFolders = vscode.workspace.workspaceFolders; for (const workspaceFolder of workspaceFolders || []) { - /// 如果根目录没有 CMakeLists.txt, 拷贝一个过去 const workspaceDir = workspaceFolder.uri.fsPath; - const cmakeListsPath = vscode.Uri.file(`${workspaceDir}/CMakeLists.txt`); - if (fs.existsSync(cmakeListsPath.fsPath)) { - ege.printInfo(t('message.cmakeListsExists')); - } else { - const cmakeListsTemplatePath = path.join(__dirname, `../cmake_template/${usingSource ? "CMakeLists_src.txt" : "CMakeLists_lib.txt"}`); - fs.copyFileSync(cmakeListsTemplatePath, cmakeListsPath.fsPath); - ege.printInfo(t('message.cmakeListsCreated')); - - /// 搜索一下项目目录下是否有别的 cpp 文件, 如果没有, 则拷贝 main.cpp + + // Handle CMakeLists.txt with content checking + const cmakeListsPath = `${workspaceDir}/CMakeLists.txt`; + const cmakeListsTemplatePath = path.join(__dirname, `../cmake_template/${usingSource ? "CMakeLists_src.txt" : "CMakeLists_lib.txt"}`); + + const cmakeResult = await copyFileWithPrompt(cmakeListsTemplatePath, cmakeListsPath); + if (cmakeResult === 'cancelled') { + ege.printInfo('操作已取消'); + return; + } + + let overwriteAll = cmakeResult === 'overwrite-all'; + let skipAll = cmakeResult === 'skip-all'; - const files = fs.readdirSync(workspaceDir, { encoding: "utf-8" }); - let hasCppFile = false; - for (const file of files) { - if (file.endsWith(".cpp")) { - hasCppFile = true; - break; - } + /// 搜索一下项目目录下是否有别的 cpp 文件, 如果没有, 则拷贝 main.cpp + const files = fs.readdirSync(workspaceDir, { encoding: "utf-8" }); + let hasCppFile = false; + for (const file of files) { + if (file.endsWith(".cpp")) { + hasCppFile = true; + break; } + } - if (!hasCppFile) { - /// 拷贝选中的 demo 文件 - const sourceFile = demoOption.fileName - ? path.join(__dirname, `../cmake_template/ege_demos/${demoOption.fileName}`) - : path.join(__dirname, `../cmake_template/main.cpp`); - - copyIfNotExist(sourceFile, `${workspaceDir}/main.cpp`); - - // If the demo requires image files, copy them too - if (demoOption.fileName === 'graph_getimage.cpp') { - const demosDir = path.join(__dirname, '../cmake_template/ege_demos'); - const imageFiles = ['getimage.jpg', 'getimage.png']; - imageFiles.forEach(imgFile => { - const imgPath = path.join(demosDir, imgFile); - if (fs.existsSync(imgPath)) { - copyIfNotExist(imgPath, `${workspaceDir}/${imgFile}`); + if (!hasCppFile) { + /// 拷贝选中的 demo 文件 + const sourceFile = demoOption.fileName + ? path.join(__dirname, `../cmake_template/ege_demos/${demoOption.fileName}`) + : path.join(__dirname, `../cmake_template/main.cpp`); + + const mainCppResult = await copyFileWithPrompt(sourceFile, `${workspaceDir}/main.cpp`, overwriteAll, skipAll); + if (mainCppResult === 'cancelled') { + ege.printInfo('操作已取消'); + return; + } + if (mainCppResult === 'overwrite-all') { + overwriteAll = true; + } else if (mainCppResult === 'skip-all') { + skipAll = true; + } + + // If the demo requires image files, copy them too + if (demoOption.fileName === 'graph_getimage.cpp') { + const demosDir = path.join(__dirname, '../cmake_template/ege_demos'); + const imageFiles = ['getimage.jpg', 'getimage.png']; + for (const imgFile of imageFiles) { + const imgPath = path.join(demosDir, imgFile); + if (fs.existsSync(imgPath)) { + const imgResult = await copyFileWithPrompt(imgPath, `${workspaceDir}/${imgFile}`, overwriteAll, skipAll); + if (imgResult === 'cancelled') { + ege.printInfo('操作已取消'); + return; } - }); + if (imgResult === 'overwrite-all') { + overwriteAll = true; + } else if (imgResult === 'skip-all') { + skipAll = true; + } + } } - } else { - ege.printInfo(t('message.skipCreateMainCpp')); } + } else { + ege.printInfo(t('message.skipCreateMainCpp')); } - // 递归拷贝(不覆盖) cmake_template/.vscode 目录下的所有内容到工作区根目录 - copyDirRecursiveIfNotExist(path.join(__dirname, `../cmake_template/.vscode`), `${workspaceDir}/.vscode`); + // Handle .vscode directory with prompt + const vscodeResult = await replaceDirWithPrompt( + path.join(__dirname, `../cmake_template/.vscode`), + `${workspaceDir}/.vscode`, + '.vscode' + ); + if (!vscodeResult) { + ege.printInfo('操作已取消'); + return; + } - /// 如果根目录没有 ege 目录, 拷贝一个过去 + // Handle ege directory with prompt const egeDir = `${workspaceDir}/ege`; - - if (!fs.existsSync(egeDir)) { - fs.mkdirpSync(egeDir); - ege.printInfo("ege 目录已创建!"); - - if (usingSource) { - const egeSrcDir = path.join(__dirname, "../bundle/ege_src"); - fs.copySync(egeSrcDir, egeDir); - } else { - /// 拷贝整个 ege 目录 - const egeLibDir = path.join(__dirname, "../bundle/ege_bundle"); - fs.copySync(egeLibDir, egeDir); - } - - } else { - ege.printInfo("ege 目录已存在, 跳过创建!"); + const egeSrcPath = usingSource + ? path.join(__dirname, "../bundle/ege_src") + : path.join(__dirname, "../bundle/ege_bundle"); + + const egeResult = await replaceDirWithPrompt(egeSrcPath, egeDir, 'ege'); + if (!egeResult) { + ege.printInfo('操作已取消'); + return; } } diff --git a/src/utils.ts b/src/utils.ts index 997b20d..090c6bd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -383,6 +383,9 @@ export function asyncRunShellCommand(command: string, args?: string[] | null, sh })); } +/** + * Copy file if not exist. If exists, skip. + */ export function copyIfNotExist(src: string, dst: string) { if (!fs.existsSync(dst)) { /// 如果中间目录不存在, 则创建 @@ -397,6 +400,89 @@ export function copyIfNotExist(src: string, dst: string) { } } +/** + * Compare file contents + */ +function filesAreIdentical(file1: string, file2: string): boolean { + try { + const content1 = fs.readFileSync(file1); + const content2 = fs.readFileSync(file2); + return content1.equals(content2); + } catch (e) { + return false; + } +} + +/** + * Copy file with user confirmation if content differs + * @returns true if file was copied or skipped (user chose not to overwrite), false if user cancelled + */ +export async function copyFileWithPrompt(src: string, dst: string, overwriteAll: boolean = false, skipAll: boolean = false): Promise<'copied' | 'skipped' | 'cancelled' | 'overwrite-all' | 'skip-all'> { + if (!fs.existsSync(dst)) { + /// 如果中间目录不存在, 则创建 + const dstDir = path.dirname(dst); + if (!fs.existsSync(dstDir)) { + fs.mkdirpSync(dstDir); + } + fs.copyFileSync(src, dst); + ege.printInfo(`${dst} 已创建!`); + return 'copied'; + } + + // File exists, check if content is identical + if (filesAreIdentical(src, dst)) { + ege.printInfo(t('message.fileContentSame', dst)); + return 'skipped'; + } + + // Content is different + ege.printInfo(t('message.fileContentDifferent', dst)); + + // If skip all is set, skip + if (skipAll) { + return 'skip-all'; + } + + // If overwrite all is set, overwrite + if (overwriteAll) { + fs.copyFileSync(src, dst); + ege.printInfo(`${dst} 已覆盖!`); + return 'overwrite-all'; + } + + // Ask user + const choice = await vscode.window.showWarningMessage( + t('prompt.overwriteFile', path.basename(dst)), + { modal: true }, + t('button.yes'), + t('button.no'), + t('button.yesToAll'), + t('button.noToAll') + ); + + if (choice === t('button.yes')) { + fs.copyFileSync(src, dst); + ege.printInfo(`${dst} 已覆盖!`); + return 'copied'; + } else if (choice === t('button.yesToAll')) { + fs.copyFileSync(src, dst); + ege.printInfo(`${dst} 已覆盖!`); + return 'overwrite-all'; + } else if (choice === t('button.noToAll')) { + ege.printInfo(`${dst} 已跳过!`); + return 'skip-all'; + } else if (choice === t('button.no')) { + ege.printInfo(`${dst} 已跳过!`); + return 'skipped'; + } else { + // User cancelled + return 'cancelled'; + } +} + +/** + * Copy directory recursively if not exist (old behavior, no prompts) + */ export function copyDirRecursiveIfNotExist(srcDir: string, dstDir: string) { const files = fs.readdirSync(srcDir, { encoding: 'utf-8' }); for (const file of files) { @@ -410,3 +496,74 @@ export function copyDirRecursiveIfNotExist(srcDir: string, dstDir: string) { } } } + +/** + * Copy directory recursively with user confirmation for each file + */ +export async function copyDirRecursiveWithPrompt(srcDir: string, dstDir: string): Promise { + let overwriteAll = false; + let skipAll = false; + + const files = fs.readdirSync(srcDir, { encoding: 'utf-8' }); + for (const file of files) { + const srcPath = path.join(srcDir, file); + const dstPath = path.join(dstDir, file); + + if (fs.statSync(srcPath).isDirectory()) { + fs.ensureDirSync(dstPath); + const result = await copyDirRecursiveWithPrompt(srcPath, dstPath); + if (!result) { + return false; // User cancelled + } + } else { + const result = await copyFileWithPrompt(srcPath, dstPath, overwriteAll, skipAll); + if (result === 'cancelled') { + return false; + } else if (result === 'overwrite-all') { + overwriteAll = true; + } else if (result === 'skip-all') { + skipAll = true; + } + } + } + + return true; +} + +/** + * Replace entire directory with user confirmation + */ +export async function replaceDirWithPrompt(srcDir: string, dstDir: string, dirName: string): Promise { + if (!fs.existsSync(dstDir)) { + // Directory doesn't exist, just copy + fs.ensureDirSync(dstDir); + fs.copySync(srcDir, dstDir); + ege.printInfo(`${dirName} 目录已创建!`); + return true; + } + + // Directory exists, ask user + ege.printInfo(t('message.directoryExists', dirName)); + + const choice = await vscode.window.showWarningMessage( + t('prompt.replaceDirectory', dirName), + { modal: true }, + t('button.yes'), + t('button.no') + ); + + if (choice === t('button.yes')) { + // Remove existing directory and copy + fs.removeSync(dstDir); + fs.ensureDirSync(dstDir); + fs.copySync(srcDir, dstDir); + ege.printInfo(`${dirName} 目录已替换!`); + return true; + } else if (choice === t('button.no')) { + ege.printInfo(`${dirName} 目录保持不变!`); + return true; + } else { + // User cancelled + return false; + } +} From 15f2da10271919bf29a2680d9aef2b7a292edb8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 08:00:09 +0000 Subject: [PATCH 7/9] Fix code review issues - proper i18n, flag propagation, and JSDoc Co-authored-by: wysaid <1430725+wysaid@users.noreply.github.com> --- src/i18n.ts | 3 +++ src/setupProject.ts | 10 +++++----- src/utils.ts | 31 +++++++++++++++++++++---------- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/i18n.ts b/src/i18n.ts index 78db229..97b6242 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -50,6 +50,7 @@ export interface LanguageStrings { 'message.fileContentDifferent': string; 'message.fileContentSame': string; 'message.directoryExists': string; + 'message.operationCancelled': string; // Prompt messages 'prompt.overwriteFile': string; @@ -118,6 +119,7 @@ const englishStrings: LanguageStrings = { 'message.fileContentDifferent': 'File {0} content is different from template', 'message.fileContentSame': 'File {0} content is identical to template, skipped', 'message.directoryExists': 'Directory {0} already exists', + 'message.operationCancelled': 'Operation cancelled', // Prompt messages 'prompt.overwriteFile': 'File {0} already exists and content is different. Overwrite?', @@ -186,6 +188,7 @@ const chineseStrings: LanguageStrings = { 'message.fileContentDifferent': '文件 {0} 的内容与模板不同', 'message.fileContentSame': '文件 {0} 内容与模板相同,已跳过', 'message.directoryExists': '目录 {0} 已存在', + 'message.operationCancelled': '操作已取消', // Prompt messages 'prompt.overwriteFile': '文件 {0} 已存在且内容不同,是否覆盖?', diff --git a/src/setupProject.ts b/src/setupProject.ts index 35da9de..cd857b4 100644 --- a/src/setupProject.ts +++ b/src/setupProject.ts @@ -35,7 +35,7 @@ export async function setupProject(usingSource?: boolean) { const cmakeResult = await copyFileWithPrompt(cmakeListsTemplatePath, cmakeListsPath); if (cmakeResult === 'cancelled') { - ege.printInfo('操作已取消'); + ege.printInfo(t('message.operationCancelled')); return; } @@ -60,7 +60,7 @@ export async function setupProject(usingSource?: boolean) { const mainCppResult = await copyFileWithPrompt(sourceFile, `${workspaceDir}/main.cpp`, overwriteAll, skipAll); if (mainCppResult === 'cancelled') { - ege.printInfo('操作已取消'); + ege.printInfo(t('message.operationCancelled')); return; } if (mainCppResult === 'overwrite-all') { @@ -78,7 +78,7 @@ export async function setupProject(usingSource?: boolean) { if (fs.existsSync(imgPath)) { const imgResult = await copyFileWithPrompt(imgPath, `${workspaceDir}/${imgFile}`, overwriteAll, skipAll); if (imgResult === 'cancelled') { - ege.printInfo('操作已取消'); + ege.printInfo(t('message.operationCancelled')); return; } if (imgResult === 'overwrite-all') { @@ -100,7 +100,7 @@ export async function setupProject(usingSource?: boolean) { '.vscode' ); if (!vscodeResult) { - ege.printInfo('操作已取消'); + ege.printInfo(t('message.operationCancelled')); return; } @@ -112,7 +112,7 @@ export async function setupProject(usingSource?: boolean) { const egeResult = await replaceDirWithPrompt(egeSrcPath, egeDir, 'ege'); if (!egeResult) { - ege.printInfo('操作已取消'); + ege.printInfo(t('message.operationCancelled')); return; } } diff --git a/src/utils.ts b/src/utils.ts index 090c6bd..93c85c5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -415,7 +415,7 @@ function filesAreIdentical(file1: string, file2: string): boolean { /** * Copy file with user confirmation if content differs - * @returns true if file was copied or skipped (user chose not to overwrite), false if user cancelled + * @returns 'copied' | 'skipped' | 'cancelled' | 'overwrite-all' | 'skip-all' */ export async function copyFileWithPrompt(src: string, dst: string, overwriteAll: boolean = false, skipAll: boolean = false): Promise<'copied' | 'skipped' | 'cancelled' | 'overwrite-all' | 'skip-all'> { if (!fs.existsSync(dst)) { @@ -499,26 +499,37 @@ export function copyDirRecursiveIfNotExist(srcDir: string, dstDir: string) { /** * Copy directory recursively with user confirmation for each file + * @param srcDir Source directory + * @param dstDir Destination directory + * @param overwriteAll Whether to overwrite all files without prompting (passed by reference via return value) + * @param skipAll Whether to skip all files without prompting (passed by reference via return value) + * @returns Object with success status and updated flags: { success: boolean, overwriteAll: boolean, skipAll: boolean } */ -export async function copyDirRecursiveWithPrompt(srcDir: string, dstDir: string): Promise { - let overwriteAll = false; - let skipAll = false; - +export async function copyDirRecursiveWithPrompt( + srcDir: string, + dstDir: string, + overwriteAll: boolean = false, + skipAll: boolean = false +): Promise<{ success: boolean, overwriteAll: boolean, skipAll: boolean }> { const files = fs.readdirSync(srcDir, { encoding: 'utf-8' }); + for (const file of files) { const srcPath = path.join(srcDir, file); const dstPath = path.join(dstDir, file); if (fs.statSync(srcPath).isDirectory()) { fs.ensureDirSync(dstPath); - const result = await copyDirRecursiveWithPrompt(srcPath, dstPath); - if (!result) { - return false; // User cancelled + const result = await copyDirRecursiveWithPrompt(srcPath, dstPath, overwriteAll, skipAll); + if (!result.success) { + return { success: false, overwriteAll, skipAll }; } + // Propagate the flags from recursive call + overwriteAll = result.overwriteAll; + skipAll = result.skipAll; } else { const result = await copyFileWithPrompt(srcPath, dstPath, overwriteAll, skipAll); if (result === 'cancelled') { - return false; + return { success: false, overwriteAll, skipAll }; } else if (result === 'overwrite-all') { overwriteAll = true; } else if (result === 'skip-all') { @@ -527,7 +538,7 @@ export async function copyDirRecursiveWithPrompt(srcDir: string, dstDir: string) } } - return true; + return { success: true, overwriteAll, skipAll }; } /** From d68a2051494b2564d820566a1dfd220c8a42a430 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:09:51 +0000 Subject: [PATCH 8/9] Fix issues: remove unwanted file, respect overwriteAll for directories, add git stash support Co-authored-by: wysaid <1430725+wysaid@users.noreply.github.com> --- .gitignore | 3 +- _codeql_detected_source_root | 1 - src/setupProject.ts | 12 +-- src/utils.ts | 161 ++++++++++++++++++++++++++++++----- 4 files changed, 151 insertions(+), 26 deletions(-) delete mode 120000 _codeql_detected_source_root diff --git a/.gitignore b/.gitignore index 9db9c57..0086afd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ build/ *.vsix package-lock.json /dist -/out \ No newline at end of file +/out +_codeql_detected_source_root \ No newline at end of file diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root deleted file mode 120000 index 945c9b4..0000000 --- a/_codeql_detected_source_root +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/src/setupProject.ts b/src/setupProject.ts index cd857b4..677b324 100644 --- a/src/setupProject.ts +++ b/src/setupProject.ts @@ -33,7 +33,7 @@ export async function setupProject(usingSource?: boolean) { const cmakeListsPath = `${workspaceDir}/CMakeLists.txt`; const cmakeListsTemplatePath = path.join(__dirname, `../cmake_template/${usingSource ? "CMakeLists_src.txt" : "CMakeLists_lib.txt"}`); - const cmakeResult = await copyFileWithPrompt(cmakeListsTemplatePath, cmakeListsPath); + const cmakeResult = await copyFileWithPrompt(cmakeListsTemplatePath, cmakeListsPath, false, false, workspaceDir); if (cmakeResult === 'cancelled') { ege.printInfo(t('message.operationCancelled')); return; @@ -58,7 +58,7 @@ export async function setupProject(usingSource?: boolean) { ? path.join(__dirname, `../cmake_template/ege_demos/${demoOption.fileName}`) : path.join(__dirname, `../cmake_template/main.cpp`); - const mainCppResult = await copyFileWithPrompt(sourceFile, `${workspaceDir}/main.cpp`, overwriteAll, skipAll); + const mainCppResult = await copyFileWithPrompt(sourceFile, `${workspaceDir}/main.cpp`, overwriteAll, skipAll, workspaceDir); if (mainCppResult === 'cancelled') { ege.printInfo(t('message.operationCancelled')); return; @@ -76,7 +76,7 @@ export async function setupProject(usingSource?: boolean) { for (const imgFile of imageFiles) { const imgPath = path.join(demosDir, imgFile); if (fs.existsSync(imgPath)) { - const imgResult = await copyFileWithPrompt(imgPath, `${workspaceDir}/${imgFile}`, overwriteAll, skipAll); + const imgResult = await copyFileWithPrompt(imgPath, `${workspaceDir}/${imgFile}`, overwriteAll, skipAll, workspaceDir); if (imgResult === 'cancelled') { ege.printInfo(t('message.operationCancelled')); return; @@ -97,7 +97,9 @@ export async function setupProject(usingSource?: boolean) { const vscodeResult = await replaceDirWithPrompt( path.join(__dirname, `../cmake_template/.vscode`), `${workspaceDir}/.vscode`, - '.vscode' + '.vscode', + overwriteAll, + workspaceDir ); if (!vscodeResult) { ege.printInfo(t('message.operationCancelled')); @@ -110,7 +112,7 @@ export async function setupProject(usingSource?: boolean) { ? path.join(__dirname, "../bundle/ege_src") : path.join(__dirname, "../bundle/ege_bundle"); - const egeResult = await replaceDirWithPrompt(egeSrcPath, egeDir, 'ege'); + const egeResult = await replaceDirWithPrompt(egeSrcPath, egeDir, 'ege', overwriteAll, workspaceDir); if (!egeResult) { ege.printInfo(t('message.operationCancelled')); return; diff --git a/src/utils.ts b/src/utils.ts index 93c85c5..92416b6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -415,9 +415,20 @@ function filesAreIdentical(file1: string, file2: string): boolean { /** * Copy file with user confirmation if content differs + * @param src Source file path + * @param dst Destination file path + * @param overwriteAll If true, overwrite without prompting + * @param skipAll If true, skip without prompting + * @param workspaceDir Workspace directory for git stash (optional) * @returns 'copied' | 'skipped' | 'cancelled' | 'overwrite-all' | 'skip-all' */ -export async function copyFileWithPrompt(src: string, dst: string, overwriteAll: boolean = false, skipAll: boolean = false): Promise<'copied' | 'skipped' | 'cancelled' | 'overwrite-all' | 'skip-all'> { +export async function copyFileWithPrompt( + src: string, + dst: string, + overwriteAll: boolean = false, + skipAll: boolean = false, + workspaceDir?: string +): Promise<'copied' | 'skipped' | 'cancelled' | 'overwrite-all' | 'skip-all'> { if (!fs.existsSync(dst)) { /// 如果中间目录不存在, 则创建 const dstDir = path.dirname(dst); @@ -445,6 +456,13 @@ export async function copyFileWithPrompt(src: string, dst: string, overwriteAll: // If overwrite all is set, overwrite if (overwriteAll) { + // Stash if git-managed + if (workspaceDir) { + const relativePath = path.relative(workspaceDir, dst); + if (relativePath && !relativePath.startsWith('..')) { + await stashFilesIfGitManaged(workspaceDir, [relativePath]); + } + } fs.copyFileSync(src, dst); ege.printInfo(`${dst} 已覆盖!`); return 'overwrite-all'; @@ -461,10 +479,24 @@ export async function copyFileWithPrompt(src: string, dst: string, overwriteAll: ); if (choice === t('button.yes')) { + // Stash if git-managed + if (workspaceDir) { + const relativePath = path.relative(workspaceDir, dst); + if (relativePath && !relativePath.startsWith('..')) { + await stashFilesIfGitManaged(workspaceDir, [relativePath]); + } + } fs.copyFileSync(src, dst); ege.printInfo(`${dst} 已覆盖!`); return 'copied'; } else if (choice === t('button.yesToAll')) { + // Stash if git-managed + if (workspaceDir) { + const relativePath = path.relative(workspaceDir, dst); + if (relativePath && !relativePath.startsWith('..')) { + await stashFilesIfGitManaged(workspaceDir, [relativePath]); + } + } fs.copyFileSync(src, dst); ege.printInfo(`${dst} 已覆盖!`); return 'overwrite-all'; @@ -503,13 +535,15 @@ export function copyDirRecursiveIfNotExist(srcDir: string, dstDir: string) { * @param dstDir Destination directory * @param overwriteAll Whether to overwrite all files without prompting (passed by reference via return value) * @param skipAll Whether to skip all files without prompting (passed by reference via return value) + * @param workspaceDir Workspace directory for git stash (optional) * @returns Object with success status and updated flags: { success: boolean, overwriteAll: boolean, skipAll: boolean } */ export async function copyDirRecursiveWithPrompt( srcDir: string, dstDir: string, overwriteAll: boolean = false, - skipAll: boolean = false + skipAll: boolean = false, + workspaceDir?: string ): Promise<{ success: boolean, overwriteAll: boolean, skipAll: boolean }> { const files = fs.readdirSync(srcDir, { encoding: 'utf-8' }); @@ -519,7 +553,7 @@ export async function copyDirRecursiveWithPrompt( if (fs.statSync(srcPath).isDirectory()) { fs.ensureDirSync(dstPath); - const result = await copyDirRecursiveWithPrompt(srcPath, dstPath, overwriteAll, skipAll); + const result = await copyDirRecursiveWithPrompt(srcPath, dstPath, overwriteAll, skipAll, workspaceDir); if (!result.success) { return { success: false, overwriteAll, skipAll }; } @@ -527,7 +561,7 @@ export async function copyDirRecursiveWithPrompt( overwriteAll = result.overwriteAll; skipAll = result.skipAll; } else { - const result = await copyFileWithPrompt(srcPath, dstPath, overwriteAll, skipAll); + const result = await copyFileWithPrompt(srcPath, dstPath, overwriteAll, skipAll, workspaceDir); if (result === 'cancelled') { return { success: false, overwriteAll, skipAll }; } else if (result === 'overwrite-all') { @@ -541,10 +575,80 @@ export async function copyDirRecursiveWithPrompt( return { success: true, overwriteAll, skipAll }; } +/** + * Check if a directory is git-managed + */ +function isGitManaged(dir: string): boolean { + try { + // Check if git is available + const gitPath = whereis('git'); + if (!gitPath) { + return false; + } + + // Check if directory is in a git repo + const result = runShellCommand('git', ['rev-parse', '--git-dir'], { cwd: dir, noErrorMsg: true }); + return result !== null && result.exitCode === 0; + } catch (e) { + return false; + } +} + +/** + * Stash files/directories before overwriting + * @param workspaceDir The workspace directory + * @param paths Paths relative to workspace to stash + */ +async function stashFilesIfGitManaged(workspaceDir: string, paths: string[]): Promise { + if (!isGitManaged(workspaceDir)) { + return; + } + + try { + // Check if there are changes to stash + const statusResult = runShellCommand('git', ['status', '--porcelain', ...paths], { cwd: workspaceDir, noErrorMsg: true }); + if (!statusResult || !statusResult.stdout || statusResult.stdout.toString().trim().length === 0) { + // No changes to stash + return; + } + + // Create a stash with a descriptive message + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const stashMessage = `EGE Plugin: Auto-stash before overwrite (${timestamp})`; + + // Stage the files first + for (const p of paths) { + runShellCommand('git', ['add', p], { cwd: workspaceDir, noErrorMsg: true }); + } + + // Create the stash + const stashResult = runShellCommand('git', ['stash', 'push', '-m', stashMessage, '--', ...paths], { cwd: workspaceDir, noErrorMsg: true }); + + if (stashResult && stashResult.exitCode === 0) { + ege.printInfo(`已使用 git stash 保存: ${paths.join(', ')}`); + } + } catch (e) { + console.error('Failed to stash files:', e); + // Don't fail the operation if stash fails + } +} + /** * Replace entire directory with user confirmation + * @param srcDir Source directory + * @param dstDir Destination directory + * @param dirName Display name for the directory + * @param overwriteAll If true, skip prompt and overwrite + * @param workspaceDir Workspace directory for git stash (optional) + * @returns true if operation succeeded or was skipped, false if cancelled */ -export async function replaceDirWithPrompt(srcDir: string, dstDir: string, dirName: string): Promise { +export async function replaceDirWithPrompt( + srcDir: string, + dstDir: string, + dirName: string, + overwriteAll: boolean = false, + workspaceDir?: string +): Promise { if (!fs.existsSync(dstDir)) { // Directory doesn't exist, just copy fs.ensureDirSync(dstDir); @@ -553,28 +657,47 @@ export async function replaceDirWithPrompt(srcDir: string, dstDir: string, dirNa return true; } - // Directory exists, ask user + // Directory exists ege.printInfo(t('message.directoryExists', dirName)); - const choice = await vscode.window.showWarningMessage( - t('prompt.replaceDirectory', dirName), - { modal: true }, - t('button.yes'), - t('button.no') - ); + let shouldReplace = overwriteAll; - if (choice === t('button.yes')) { + if (!overwriteAll) { + // Ask user + const choice = await vscode.window.showWarningMessage( + t('prompt.replaceDirectory', dirName), + { modal: true }, + t('button.yes'), + t('button.no') + ); + + if (choice === t('button.yes')) { + shouldReplace = true; + } else if (choice === t('button.no')) { + ege.printInfo(`${dirName} 目录保持不变!`); + return true; + } else { + // User cancelled + return false; + } + } + + if (shouldReplace) { + // Stash if git-managed + if (workspaceDir) { + const relativePath = path.relative(workspaceDir, dstDir); + if (relativePath && !relativePath.startsWith('..')) { + await stashFilesIfGitManaged(workspaceDir, [relativePath]); + } + } + // Remove existing directory and copy fs.removeSync(dstDir); fs.ensureDirSync(dstDir); fs.copySync(srcDir, dstDir); ege.printInfo(`${dirName} 目录已替换!`); return true; - } else if (choice === t('button.no')) { - ege.printInfo(`${dirName} 目录保持不变!`); - return true; - } else { - // User cancelled - return false; } + + return true; } From dc496c5c39c4589e7a2e7e05b35e685ef8ed347b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:11:59 +0000 Subject: [PATCH 9/9] Fix code review issue - improve stdout type handling in git stash Co-authored-by: wysaid <1430725+wysaid@users.noreply.github.com> --- src/utils.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 92416b6..0a1b9f5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -607,7 +607,13 @@ async function stashFilesIfGitManaged(workspaceDir: string, paths: string[]): Pr try { // Check if there are changes to stash const statusResult = runShellCommand('git', ['status', '--porcelain', ...paths], { cwd: workspaceDir, noErrorMsg: true }); - if (!statusResult || !statusResult.stdout || statusResult.stdout.toString().trim().length === 0) { + if (!statusResult || !statusResult.stdout) { + // No changes to stash + return; + } + + const stdout = typeof statusResult.stdout === 'string' ? statusResult.stdout : statusResult.stdout.toString(); + if (stdout.trim().length === 0) { // No changes to stash return; }