├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── assets ├── powerrune.gif └── powerrune_window.png └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | cmake-build-debug 3 | .idea 4 | .vscode 5 | .cache -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.1) 2 | project(power_rune) 3 | set(CMAKE_CXX_STANDARD 14) 4 | 5 | find_package(OpenCV 4 REQUIRED) 6 | include_directories(${OpenCV_INCLUDE_DIRS}) 7 | 8 | add_executable(power_rune main.cpp) 9 | target_link_libraries(power_rune ${OpenCV_LIBS}) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 UoN-Lancet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Power Rune Simulator 2 | 3 | ![](assets/powerrune.gif) 4 | ![](assets/powerrune_window.png) 5 | 6 | ## Dependencies 7 | 8 | ```shell 9 | $ sudo apt install opencv-data libopencv-dev libeigen3-dev 10 | ``` 11 | 12 | ## Build & Run 13 | 14 | ```shell 15 | $ git clone https://github.com/UoN-Lancet/PowerRuneSimulator.git 16 | $ cd PowerRuneSimulator 17 | $ mkdir -p build && cd build 18 | $ cmake .. 19 | $ make 20 | $ ./power_rune 21 | ``` 22 | -------------------------------------------------------------------------------- /assets/powerrune.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RIvance/PowerRuneSimulator/e585a41793ca0c07a55db14393525eebb4dbd05e/assets/powerrune.gif -------------------------------------------------------------------------------- /assets/powerrune_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RIvance/PowerRuneSimulator/e585a41793ca0c07a55db14393525eebb4dbd05e/assets/powerrune_window.png -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | using SysClock = std::chrono::system_clock; 9 | using Timestamp = SysClock::time_point; 10 | template using Duration = std::chrono::duration; 11 | template using Vector = std::vector; 12 | 13 | struct GraphicLine 14 | { 15 | cv::Point p1, p2; 16 | int label; 17 | }; 18 | 19 | struct GeometryLine 20 | { 21 | Eigen::Vector2d p1, p2; 22 | int label = 0; 23 | 24 | GraphicLine graphic(const cv::Size & imageSize) const 25 | { 26 | return { 27 | cv::Point( 28 | (int) p1(0) + imageSize.width / 2, 29 | (int) -p1(1) + imageSize.height / 2 30 | ), 31 | cv::Point( 32 | (int) p2(0) + imageSize.width / 2, 33 | (int) -p2(1) + imageSize.height / 2 34 | ), 35 | label 36 | }; 37 | } 38 | 39 | GeometryLine transform(const Eigen::Matrix2d & mat) const 40 | { 41 | return GeometryLine(mat * p1, mat * p2, label); 42 | } 43 | 44 | GeometryLine() = default; 45 | 46 | explicit GeometryLine(Eigen::Vector2d p1, Eigen::Vector2d p2, int label) 47 | : p1(std::move(p1)), p2(std::move(p2)), label(label) 48 | { /* empty */ } 49 | 50 | explicit GeometryLine( 51 | double x1, double y1, double x2, double y2, 52 | int label = 0 53 | ) { 54 | p1 << x1, y1; 55 | p2 << x2, y2; 56 | this->label = label; 57 | } 58 | }; 59 | 60 | class Geometry 61 | { 62 | private: 63 | 64 | Vector lines; 65 | 66 | public: 67 | 68 | void rotate(double angle) 69 | { 70 | Eigen::Matrix2d rotateMatrix; 71 | rotateMatrix << std::cos(angle), std::sin(angle), 72 | -std::sin(angle), std::cos(angle); 73 | for (GeometryLine & line : lines) { 74 | line = line.transform(rotateMatrix); 75 | } 76 | } 77 | 78 | void scale(double scl) 79 | { 80 | for (GeometryLine & line : lines) { 81 | line.p1 *= scl; 82 | line.p2 *= scl; 83 | } 84 | } 85 | 86 | void addLine(const GeometryLine & line) 87 | { 88 | this->lines.emplace_back(line); 89 | } 90 | 91 | Vector getGraphic(const cv::Size & imageSize) const 92 | { 93 | Vector graphicLines; 94 | for (const GeometryLine & line : lines) { 95 | graphicLines.emplace_back(line.graphic(imageSize)); 96 | } 97 | return graphicLines; 98 | } 99 | 100 | Geometry() = default; 101 | 102 | explicit Geometry(Vector lines) 103 | : lines(std::move(lines)) 104 | { /* empty */ } 105 | }; 106 | 107 | enum LightBlobLabel 108 | { 109 | LIGHT_BLOB_MASK = 0, 110 | LIGHT_BLOB_ARMOR_HORIZONTAL = 1, 111 | LIGHT_BLOB_ARMOR_VERTICAL = 2, 112 | LIGHT_BLOB_ARM_STICK = 3, 113 | LIGHT_BLOB_OUTSIDE = 4, 114 | }; 115 | 116 | class PowerRuneArm 117 | { 118 | public: 119 | 120 | enum State 121 | { 122 | DEACTIVATED = 0, 123 | ACTIVATING = 1, 124 | ACTIVATED = 2, 125 | }; 126 | 127 | private: 128 | 129 | const Geometry geometry = Geometry({ 130 | GeometryLine(0, 20, 0, 66, LIGHT_BLOB_ARM_STICK), 131 | GeometryLine(-13, 85, 13, 85, LIGHT_BLOB_ARMOR_HORIZONTAL), 132 | GeometryLine(-14, 68, 14, 68, LIGHT_BLOB_ARMOR_HORIZONTAL), 133 | GeometryLine(-13, 85, -14, 68, LIGHT_BLOB_ARMOR_VERTICAL), 134 | GeometryLine(13, 85, 14, 68, LIGHT_BLOB_ARMOR_VERTICAL), 135 | GeometryLine(-6, 18, 6, 18, LIGHT_BLOB_OUTSIDE), // `_` 136 | GeometryLine(6, 18, 16, 31, LIGHT_BLOB_OUTSIDE), // `/` 137 | GeometryLine(-6, 18, -16, 31, LIGHT_BLOB_OUTSIDE), // `\` 138 | GeometryLine(16, 31, 14, 68, LIGHT_BLOB_OUTSIDE), // `|` 139 | GeometryLine(-16, 31, -14, 68, LIGHT_BLOB_OUTSIDE), // `|` 140 | }); 141 | 142 | State state = DEACTIVATED; 143 | Timestamp createdTime = SysClock::now(); 144 | 145 | static double maskYOffset(double t) 146 | { 147 | t *= 2; 148 | return 6 * (t - floor(t)); 149 | } 150 | 151 | public: 152 | 153 | int id = 0; 154 | 155 | State activateState() const 156 | { 157 | return this->state; 158 | } 159 | 160 | void light() 161 | { 162 | state = ACTIVATING; 163 | } 164 | 165 | void activate() 166 | { 167 | state = ACTIVATED; 168 | } 169 | 170 | void deactivate() 171 | { 172 | state = DEACTIVATED; 173 | } 174 | 175 | Geometry getStickMask(double angle = 0, double scale = 1) const 176 | { 177 | double t = Duration(SysClock::now() - createdTime).count(); 178 | double offset = maskYOffset(t); 179 | GeometryLine lineLeft = GeometryLine(0, 6 + offset, -6, 0 + offset, LIGHT_BLOB_MASK); 180 | GeometryLine lineRight = GeometryLine(0, 6 + offset, 6, 0 + offset, LIGHT_BLOB_MASK); 181 | 182 | Geometry mask; 183 | 184 | for (int i = 0; i < 10; i++) { 185 | lineLeft.p1(1) += 6; 186 | lineLeft.p2(1) += 6; 187 | lineRight.p1(1) += 6; 188 | lineRight.p2(1) += 6; 189 | mask.addLine(lineLeft); 190 | mask.addLine(lineRight); 191 | } 192 | 193 | mask.rotate(this->id * CV_PI * (2.0 / 5.0) + angle); 194 | mask.scale(scale); 195 | 196 | return mask; 197 | } 198 | 199 | Geometry getGeometry(double angle = 0, double scale = 1) const 200 | { 201 | Geometry geo = this->geometry; 202 | geo.rotate(this->id * CV_PI * (2.0 / 5.0) + angle); 203 | geo.scale(scale); 204 | return geo; 205 | } 206 | 207 | PowerRuneArm() = default; 208 | 209 | PowerRuneArm(const PowerRuneArm & arm) 210 | { 211 | this->id = arm.id; 212 | this->state = arm.state; 213 | } 214 | 215 | }; 216 | 217 | class ClockSin 218 | { 219 | private: 220 | 221 | Timestamp lastTimeStamp = SysClock::now(); 222 | // t: sec 223 | double t = 0; 224 | 225 | public: 226 | 227 | double A, w, b; 228 | ClockSin(double a, double w, double b) : A(a), w(w), b(b) {} 229 | 230 | double operator() () 231 | { 232 | Timestamp current = SysClock::now(); 233 | this->t += Duration(current - lastTimeStamp).count(); 234 | double value = A * std::sin(w * t) + b; 235 | lastTimeStamp = current; 236 | return value; 237 | } 238 | 239 | double integral() 240 | { 241 | Timestamp current = SysClock::now(); 242 | double deltaT = Duration(current - lastTimeStamp).count(); 243 | t += deltaT; 244 | double value = (A * std::sin(w * t) + b) * deltaT; 245 | lastTimeStamp = current; 246 | return value; 247 | } 248 | }; 249 | 250 | enum Color 251 | { 252 | RED, 253 | BLUE, 254 | }; 255 | 256 | class PowerRune 257 | { 258 | private: 259 | 260 | PowerRuneArm arms[5]; 261 | std::mt19937 mt = std::mt19937(std::random_device()()); 262 | int activeOrder[5] = { 0, 1, 2, 3, 4 }; 263 | int activating = 0; 264 | double angle = 0; 265 | double scale = 3; 266 | ClockSin clockSin; 267 | Color color; 268 | double bloomFactor = 1.4; 269 | 270 | void next() 271 | { 272 | this->angle += clockSin.integral(); 273 | } 274 | 275 | cv::Mat bloom(const cv::Mat & src) const 276 | { 277 | cv::Mat gaussian, image; 278 | cv::GaussianBlur(src, image, cv::Size(3, 3), 1); 279 | int gaussianKernelSize = (int) (9.0 * scale); 280 | gaussianKernelSize += gaussianKernelSize % 2 == 0 ? 1 : 0; 281 | cv::GaussianBlur(image, gaussian, cv::Size(gaussianKernelSize, gaussianKernelSize), 8); 282 | return image + gaussian * bloomFactor; 283 | } 284 | 285 | public: 286 | 287 | cv::Mat renderImage() 288 | { 289 | this->next(); 290 | auto imageSize = cv::Size((int) (scale * 200), (int) (scale * 200)); 291 | cv::Mat image = cv::Mat::zeros(imageSize, CV_8UC3); 292 | cv::Scalar rgbColor = this->color == RED ? cv::Scalar(50, 70, 255) : cv::Scalar(255, 70, 50); 293 | 294 | for (const auto & arm : arms) { 295 | auto geo = arm.getGeometry(this->angle, this->scale); 296 | for (const GraphicLine & line : geo.getGraphic(image.size())) { 297 | switch (line.label) { 298 | case LIGHT_BLOB_ARMOR_HORIZONTAL: { 299 | if (arm.activateState() != PowerRuneArm::DEACTIVATED) { 300 | cv::line(image, line.p1, line.p2, rgbColor, scale * 2, cv::LINE_AA); 301 | } 302 | } break; 303 | 304 | case LIGHT_BLOB_ARMOR_VERTICAL: { 305 | if (arm.activateState() != PowerRuneArm::DEACTIVATED) { 306 | cv::line(image, line.p1, line.p2, rgbColor, scale * 1.5, cv::LINE_AA); 307 | } 308 | } break; 309 | 310 | case LIGHT_BLOB_ARM_STICK: { 311 | if (arm.activateState() == PowerRuneArm::ACTIVATED) { 312 | cv::line(image, line.p1, line.p2, rgbColor, scale * 4, cv::LINE_AA); 313 | } else if (arm.activateState() == PowerRuneArm::ACTIVATING) { 314 | cv::line(image, line.p1, line.p2, rgbColor, scale * 4, cv::LINE_AA); 315 | for (GraphicLine & mask : arm.getStickMask(this->angle, this->scale).getGraphic(imageSize)) { 316 | const cv::Scalar BLACK(0, 0, 0); 317 | cv::line(image, mask.p1, mask.p2, BLACK, scale * 2.4, cv::LINE_AA); 318 | } 319 | } 320 | } break; 321 | 322 | case LIGHT_BLOB_OUTSIDE: { 323 | if (arm.activateState() == PowerRuneArm::ACTIVATED) { 324 | cv::line(image, line.p1, line.p2, rgbColor, scale * 1, cv::LINE_AA); 325 | } 326 | } break; 327 | } 328 | } 329 | } 330 | return bloom(image); 331 | } 332 | 333 | void switchColor() 334 | { 335 | if (this->color == RED) { 336 | this->color = BLUE; 337 | } else { 338 | this->color = RED; 339 | } 340 | } 341 | 342 | void setBloom(double factor) 343 | { 344 | this->bloomFactor = factor; 345 | } 346 | 347 | void setScale(double value) 348 | { 349 | this->scale = value; 350 | } 351 | 352 | void setSin_A(double value) 353 | { 354 | this->clockSin.A = value; 355 | } 356 | 357 | void setSin_w(double value) 358 | { 359 | this->clockSin.w = value; 360 | } 361 | 362 | void setSin_b(double value) 363 | { 364 | this->clockSin.b = value; 365 | } 366 | 367 | void activateNext() 368 | { 369 | if (activating == 6) { 370 | std::shuffle(activeOrder, activeOrder + 5, mt); 371 | for (auto & arm : arms) { 372 | arm.deactivate(); 373 | } 374 | activating = 0; 375 | arms[activeOrder[0]].light(); 376 | } else { 377 | arms[activeOrder[activating++]].activate(); 378 | if (activating == 5) { 379 | activating += 1; 380 | } else { 381 | arms[activeOrder[activating]].light(); 382 | } 383 | } 384 | } 385 | 386 | explicit PowerRune(double A, double w, double b, Color color) 387 | noexcept : clockSin(A, w, b), color(color) 388 | { 389 | for (int i = 0; i < 5; i++) { 390 | arms[i].id = i; 391 | arms[i].activate(); 392 | } 393 | std::shuffle(activeOrder, activeOrder + 5, mt); 394 | activating = 6; 395 | } 396 | 397 | }; 398 | 399 | PowerRune powerRune(0.785, 1.884, 1.305, RED); 400 | 401 | void onMouseClick(int event, int, int, int, void*) 402 | { 403 | if (event == cv::EVENT_LBUTTONDOWN) { 404 | powerRune.activateNext(); 405 | } 406 | } 407 | 408 | void onColorChange(int, void*) 409 | { 410 | powerRune.switchColor(); 411 | } 412 | 413 | void onBloomChange(int value, void*) 414 | { 415 | powerRune.setBloom(0.5 + value / 25.0); 416 | } 417 | 418 | void onSizeChange(int value, void*) 419 | { 420 | powerRune.setScale(value * 0.1 + 1); 421 | } 422 | 423 | 424 | #define WIN_NAME "PowerRune :: A sin(wt + b)" 425 | #define SIN_TRACKBAR(var_, expr_) \ 426 | cv::createTrackbar(#var_" "#expr_, WIN_NAME, nullptr, 100, [](int (var_), void*) { \ 427 | powerRune.setSin_##var_ (var_ expr_); \ 428 | }) \ 429 | 430 | void initializeWindow() 431 | { 432 | cv::namedWindow(WIN_NAME); 433 | cv::setMouseCallback(WIN_NAME, onMouseClick); 434 | 435 | SIN_TRACKBAR(A, * 0.05); 436 | SIN_TRACKBAR(w, * 0.05); 437 | SIN_TRACKBAR(b, * 0.1 - 5); 438 | 439 | cv::createTrackbar("color", WIN_NAME, nullptr, 1, onColorChange); 440 | cv::createTrackbar("brightness", WIN_NAME, nullptr, 100, onBloomChange); 441 | cv::createTrackbar("size", WIN_NAME, nullptr, 300, onSizeChange); 442 | } 443 | 444 | [[noreturn]] 445 | int main() 446 | { 447 | initializeWindow(); 448 | while (true) { 449 | cv::imshow(WIN_NAME, powerRune.renderImage()); 450 | cv::waitKey(1); 451 | } 452 | } 453 | --------------------------------------------------------------------------------