From 8802a908dfe344196068c3a5922f8e4af781a456 Mon Sep 17 00:00:00 2001 From: Dao Heng Liu Date: Thu, 15 Jul 2021 11:20:38 +0100 Subject: [PATCH 1/9] make frame renderer and video encoder work in parallel, use IPC rather than temporary files for communication (WIP) (cherry picked from commit a29b2963bd98b408d90c41308bed2c65c526a4ca) --- commands/render.js | 43 +- package-lock.json | 2360 +------------------------------------ package.json | 2 + renderer/render_frame.js | 825 +++++++------ renderer/render_worker.js | 1976 ++++++++++++++++--------------- 5 files changed, 1612 insertions(+), 3594 deletions(-) diff --git a/commands/render.js b/commands/render.js index 3aa1496..04505df 100644 --- a/commands/render.js +++ b/commands/render.js @@ -41,9 +41,9 @@ module.exports = { let { argv, msg, last_beatmap } = obj; let beatmap_id, beatmap_url, beatmap_promise, mods = [], time = 0, - ar, cs, od, length = 0, percent = 0, custom_url = false, + ar, cs, od, length = 0, percent = 0, custom_url = false, nobg = false, bg_opacity = 20, size = [400, 300], type, objects, - video_type = 'gif', audio = true, download_promise, osr; + video_type = 'gif', audio = true, download_promise, osr, offset; let score_id; @@ -107,7 +107,6 @@ module.exports = { audio = false; }else if(arg.endsWith('%')){ speed = parseInt(arg) / 100; - speed = Math.max(0.01, speed); }else if(arg.endsWith('fps')){ let _fps = parseInt(arg); if(!isNaN(_fps)){ @@ -126,9 +125,11 @@ module.exports = { ar = parseFloat(arg.substr(2)); }else if(arg.toLowerCase().startsWith('cs')){ cs = parseFloat(arg.substr(2)); - }else if(arg.toLowerCase().startsWith('od')){ - od = parseFloat(arg.substr(2)); - }else if(arg.startsWith('(') && arg.endsWith(')')){ + }else if(arg.toLowerCase().startsWith('od')) { + od = parseFloat(arg.substr(2)); + }else if(arg.toLowerCase().endsWith('offset')){ + offset = parseInt(arg) * 1000; + }else if(arg.startsWith('(') && arg.endsWith(')')){ objects = arg.substr(1, arg.length - 1).split(',').length; }else if(arg == 'fail'){ if(msg.channel.id in last_beatmap){ @@ -140,7 +141,11 @@ module.exports = { percent = last_beatmap[msg.channel.id].fail_percent; length = 4; } - }else{ + } else if(arg == 'nobg') { + nobg = true; + } else if(arg.toLowerCase().endsWith('bgo')){ + bg_opacity = parseInt(arg); + }else{ if(arg.startsWith('http://') || arg.startsWith('https://')){ beatmap_url = arg; beatmap_promise = osu.parse_beatmap_url(beatmap_url); @@ -210,8 +215,28 @@ module.exports = { frame.get_frames(download_path, time, length * 1000, mods, size, { combo, - type: video_type, cs, ar, od, analyze, hidden, flashlight, black: false, osr, score_id, audio, fps, speed, - fill: video_type == 'mp4', noshadow: true, percent, border: false, objects, msg + type: video_type, + cs, + ar, + od, + analyze, + hidden, + flashlight, + black: false, + osr, + score_id, + audio, + fps, + speed, + fill: video_type === 'mp4', + noshadow: true, + percent, + offset, + nobg, + bg_opacity, + border: false, + objects, + msg }); }else{ frame.get_frame(download_path, time, mods, [800, 600], { diff --git a/package-lock.json b/package-lock.json index ab86f19..2ca7bd6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2317 +1,8 @@ { "name": "flowabot", "version": "1.0.0", - "lockfileVersion": 2, + "lockfileVersion": 1, "requires": true, - "packages": { - "": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "axios": "^0.21.1", - "canvas": "^2.7.0", - "chalk": "^4.1.0", - "chart.js": "^2.9.4", - "chartjs-node-canvas": "^3.1.0", - "cheerio": "^1.0.0-rc.6", - "discord.js": "^12.5.3", - "diskusage": "^1.1.3", - "ffmpeg-static": "^4.3.0", - "jimp": "^0.16.1", - "lodash": "^4.17.21", - "luxon": "^1.26.0", - "lzma-native": "^7.0.1", - "mathjs": "^9.3.0", - "node-emoji": "^1.10.0", - "node-fetch": "^2.6.1", - "node-localstorage": "^2.1.6", - "node-osr": "^1.2.1", - "object-path": "^0.11.5", - "ojsama": "^2.2.0", - "osu-parser": "git+https://github.com/LeaPhant/osu-parser.git", - "readline-sync": "^1.4.10", - "semver": "^7.3.5", - "tz-lookup": "^6.1.25", - "unzipper": "^0.10.11", - "vm2": "^3.9.3" - } - }, - "node_modules/@babel/runtime": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", - "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==", - "dependencies": { - "regenerator-runtime": "^0.13.4" - } - }, - "node_modules/@derhuerst/http-basic": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.1.tgz", - "integrity": "sha512-Rmn7qQQulw2sxJ8qGfZ7OuqMWuhz8V+L5xnYKMF5cXVcYqmgWqlVEAme90pF7Ya8OVhxVxLmhh0rI2k6t7ITWw==", - "dependencies": { - "caseless": "^0.12.0", - "concat-stream": "^1.6.2", - "http-response-object": "^3.0.1", - "parse-cache-control": "^1.0.1" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@derhuerst/http-basic/node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/@discordjs/collection": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz", - "integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==" - }, - "node_modules/@discordjs/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@jimp/bmp": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.16.1.tgz", - "integrity": "sha512-iwyNYQeBawrdg/f24x3pQ5rEx+/GwjZcCXd3Kgc+ZUd+Ivia7sIqBsOnDaMZdKCBPlfW364ekexnlOqyVa0NWg==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1", - "bmp-js": "^0.1.0" - } - }, - "node_modules/@jimp/core": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.16.1.tgz", - "integrity": "sha512-la7kQia31V6kQ4q1kI/uLimu8FXx7imWVajDGtwUG8fzePLWDFJyZl0fdIXVCL1JW2nBcRHidUot6jvlRDi2+g==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1", - "any-base": "^1.1.0", - "buffer": "^5.2.0", - "exif-parser": "^0.1.12", - "file-type": "^9.0.0", - "load-bmfont": "^1.3.1", - "mkdirp": "^0.5.1", - "phin": "^2.9.1", - "pixelmatch": "^4.0.2", - "tinycolor2": "^1.4.1" - } - }, - "node_modules/@jimp/custom": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.16.1.tgz", - "integrity": "sha512-DNUAHNSiUI/j9hmbatD6WN/EBIyeq4AO0frl5ETtt51VN1SvE4t4v83ZA/V6ikxEf3hxLju4tQ5Pc3zmZkN/3A==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/core": "^0.16.1" - } - }, - "node_modules/@jimp/gif": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.16.1.tgz", - "integrity": "sha512-r/1+GzIW1D5zrP4tNrfW+3y4vqD935WBXSc8X/wm23QTY9aJO9Lw6PEdzpYCEY+SOklIFKaJYUAq/Nvgm/9ryw==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1", - "gifwrap": "^0.9.2", - "omggif": "^1.0.9" - } - }, - "node_modules/@jimp/jpeg": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.16.1.tgz", - "integrity": "sha512-8352zrdlCCLFdZ/J+JjBslDvml+fS3Z8gttdml0We759PnnZGqrnPRhkOEOJbNUlE+dD4ckLeIe6NPxlS/7U+w==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1", - "jpeg-js": "0.4.2" - } - }, - "node_modules/@jimp/plugin-blit": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.16.1.tgz", - "integrity": "sha512-fKFNARm32RoLSokJ8WZXHHH2CGzz6ire2n1Jh6u+XQLhk9TweT1DcLHIXwQMh8oR12KgjbgsMGvrMVlVknmOAg==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-blur": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.16.1.tgz", - "integrity": "sha512-1WhuLGGj9MypFKRcPvmW45ht7nXkOKu+lg3n2VBzIB7r4kKNVchuI59bXaCYQumOLEqVK7JdB4glaDAbCQCLyw==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-circle": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.16.1.tgz", - "integrity": "sha512-JK7yi1CIU7/XL8hdahjcbGA3V7c+F+Iw+mhMQhLEi7Q0tCnZ69YJBTamMiNg3fWPVfMuvWJJKOBRVpwNTuaZRg==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-color": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.16.1.tgz", - "integrity": "sha512-9yQttBAO5SEFj7S6nJK54f+1BnuBG4c28q+iyzm1JjtnehjqMg6Ljw4gCSDCvoCQ3jBSYHN66pmwTV74SU1B7A==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1", - "tinycolor2": "^1.4.1" - } - }, - "node_modules/@jimp/plugin-contain": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.16.1.tgz", - "integrity": "sha512-44F3dUIjBDHN+Ym/vEfg+jtjMjAqd2uw9nssN67/n4FdpuZUVs7E7wadKY1RRNuJO+WgcD5aDQcsvurXMETQTg==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-cover": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.16.1.tgz", - "integrity": "sha512-YztWCIldBAVo0zxcQXR+a/uk3/TtYnpKU2CanOPJ7baIuDlWPsG+YE4xTsswZZc12H9Kl7CiziEbDtvF9kwA/Q==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-crop": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.16.1.tgz", - "integrity": "sha512-UQdva9oQzCVadkyo3T5Tv2CUZbf0klm2cD4cWMlASuTOYgaGaFHhT9st+kmfvXjKL8q3STkBu/zUPV6PbuV3ew==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-displace": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.16.1.tgz", - "integrity": "sha512-iVAWuz2+G6Heu8gVZksUz+4hQYpR4R0R/RtBzpWEl8ItBe7O6QjORAkhxzg+WdYLL2A/Yd4ekTpvK0/qW8hTVw==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-dither": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.16.1.tgz", - "integrity": "sha512-tADKVd+HDC9EhJRUDwMvzBXPz4GLoU6s5P7xkVq46tskExYSptgj5713J5Thj3NMgH9Rsqu22jNg1H/7tr3V9Q==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-fisheye": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.16.1.tgz", - "integrity": "sha512-BWHnc5hVobviTyIRHhIy9VxI1ACf4CeSuCfURB6JZm87YuyvgQh5aX5UDKtOz/3haMHXBLP61ZBxlNpMD8CG4A==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-flip": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.16.1.tgz", - "integrity": "sha512-KdxTf0zErfZ8DyHkImDTnQBuHby+a5YFdoKI/G3GpBl3qxLBvC+PWkS2F/iN3H7wszP7/TKxTEvWL927pypT0w==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-gaussian": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.16.1.tgz", - "integrity": "sha512-u9n4wjskh3N1mSqketbL6tVcLU2S5TEaFPR40K6TDv4phPLZALi1Of7reUmYpVm8mBDHt1I6kGhuCJiWvzfGyg==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-invert": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.16.1.tgz", - "integrity": "sha512-2DKuyVXANH8WDpW9NG+PYFbehzJfweZszFYyxcaewaPLN0GxvxVLOGOPP1NuUTcHkOdMFbE0nHDuB7f+sYF/2w==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-mask": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.16.1.tgz", - "integrity": "sha512-snfiqHlVuj4bSFS0v96vo2PpqCDMe4JB+O++sMo5jF5mvGcGL6AIeLo8cYqPNpdO6BZpBJ8MY5El0Veckhr39Q==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-normalize": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.16.1.tgz", - "integrity": "sha512-dOQfIOvGLKDKXPU8xXWzaUeB0nvkosHw6Xg1WhS1Z5Q0PazByhaxOQkSKgUryNN/H+X7UdbDvlyh/yHf3ITRaw==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-print": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.16.1.tgz", - "integrity": "sha512-ceWgYN40jbN4cWRxixym+csyVymvrryuKBQ+zoIvN5iE6OyS+2d7Mn4zlNgumSczb9GGyZZESIgVcBDA1ezq0Q==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1", - "load-bmfont": "^1.4.0" - } - }, - "node_modules/@jimp/plugin-resize": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.16.1.tgz", - "integrity": "sha512-u4JBLdRI7dargC04p2Ha24kofQBk3vhaf0q8FwSYgnCRwxfvh2RxvhJZk9H7Q91JZp6wgjz/SjvEAYjGCEgAwQ==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-rotate": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.16.1.tgz", - "integrity": "sha512-ZUU415gDQ0VjYutmVgAYYxC9Og9ixu2jAGMCU54mSMfuIlmohYfwARQmI7h4QB84M76c9hVLdONWjuo+rip/zg==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-scale": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.16.1.tgz", - "integrity": "sha512-jM2QlgThIDIc4rcyughD5O7sOYezxdafg/2Xtd1csfK3z6fba3asxDwthqPZAgitrLgiKBDp6XfzC07Y/CefUw==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-shadow": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.16.1.tgz", - "integrity": "sha512-MeD2Is17oKzXLnsphAa1sDstTu6nxscugxAEk3ji0GV1FohCvpHBcec0nAq6/czg4WzqfDts+fcPfC79qWmqrA==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugin-threshold": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.16.1.tgz", - "integrity": "sha512-iGW8U/wiCSR0+6syrPioVGoSzQFt4Z91SsCRbgNKTAk7D+XQv6OI78jvvYg4o0c2FOlwGhqz147HZV5utoSLxA==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1" - } - }, - "node_modules/@jimp/plugins": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.16.1.tgz", - "integrity": "sha512-c+lCqa25b+4q6mJZSetlxhMoYuiltyS+ValLzdwK/47+aYsq+kcJNl+TuxIEKf59yr9+5rkbpsPkZHLF/V7FFA==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/plugin-blit": "^0.16.1", - "@jimp/plugin-blur": "^0.16.1", - "@jimp/plugin-circle": "^0.16.1", - "@jimp/plugin-color": "^0.16.1", - "@jimp/plugin-contain": "^0.16.1", - "@jimp/plugin-cover": "^0.16.1", - "@jimp/plugin-crop": "^0.16.1", - "@jimp/plugin-displace": "^0.16.1", - "@jimp/plugin-dither": "^0.16.1", - "@jimp/plugin-fisheye": "^0.16.1", - "@jimp/plugin-flip": "^0.16.1", - "@jimp/plugin-gaussian": "^0.16.1", - "@jimp/plugin-invert": "^0.16.1", - "@jimp/plugin-mask": "^0.16.1", - "@jimp/plugin-normalize": "^0.16.1", - "@jimp/plugin-print": "^0.16.1", - "@jimp/plugin-resize": "^0.16.1", - "@jimp/plugin-rotate": "^0.16.1", - "@jimp/plugin-scale": "^0.16.1", - "@jimp/plugin-shadow": "^0.16.1", - "@jimp/plugin-threshold": "^0.16.1", - "timm": "^1.6.1" - } - }, - "node_modules/@jimp/png": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.16.1.tgz", - "integrity": "sha512-iyWoCxEBTW0OUWWn6SveD4LePW89kO7ZOy5sCfYeDM/oTPLpR8iMIGvZpZUz1b8kvzFr27vPst4E5rJhGjwsdw==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.1", - "pngjs": "^3.3.3" - } - }, - "node_modules/@jimp/tiff": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.16.1.tgz", - "integrity": "sha512-3K3+xpJS79RmSkAvFMgqY5dhSB+/sxhwTFA9f4AVHUK0oKW+u6r52Z1L0tMXHnpbAdR9EJ+xaAl2D4x19XShkQ==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "utif": "^2.0.1" - } - }, - "node_modules/@jimp/types": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.16.1.tgz", - "integrity": "sha512-g1w/+NfWqiVW4CaXSJyD28JQqZtm2eyKMWPhBBDCJN9nLCN12/Az0WFF3JUAktzdsEC2KRN2AqB1a2oMZBNgSQ==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/bmp": "^0.16.1", - "@jimp/gif": "^0.16.1", - "@jimp/jpeg": "^0.16.1", - "@jimp/png": "^0.16.1", - "@jimp/tiff": "^0.16.1", - "timm": "^1.6.1" - } - }, - "node_modules/@jimp/utils": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.16.1.tgz", - "integrity": "sha512-8fULQjB0x4LzUSiSYG6ZtQl355sZjxbv8r9PPAuYHzS9sGiSHJQavNqK/nKnpDsVkU88/vRGcE7t3nMU0dEnVw==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "regenerator-runtime": "^0.13.3" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.3.tgz", - "integrity": "sha512-9dTIfQW8HVCxLku5QrJ/ysS/b2MdYngs9+/oPrOTLvp3TrggdANYVW2h8FGJGDf0J7MYfp44W+c90cVJx+ASuA==", - "dependencies": { - "detect-libc": "^1.0.3", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.1", - "nopt": "^5.0.0", - "npmlog": "^4.1.2", - "rimraf": "^3.0.2", - "semver": "^7.3.4", - "tar": "^6.1.0" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@types/node": { - "version": "10.17.56", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.56.tgz", - "integrity": "sha512-LuAa6t1t0Bfw4CuSR0UITsm1hP17YL+u82kfHGrHUWdhlBtH7sa7jGY5z7glGaIj/WDYDkRtgGd+KCjCzxBW1w==" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/agent-base/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/any-base": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", - "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==" - }, - "node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "node_modules/are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "node_modules/axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", - "dependencies": { - "follow-redirects": "^1.10.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "node_modules/big-integer": { - "version": "1.6.48", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", - "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" - }, - "node_modules/bmp-js": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", - "integrity": "sha1-4Fpj95amwf8l9Hcex62twUjAcjM=" - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-equal": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", - "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "engines": { - "node": ">=0.2.0" - } - }, - "node_modules/canvas": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.7.0.tgz", - "integrity": "sha512-pzCxtkHb+5su5MQjTtepMDlIOtaXo277x0C0u3nMOxtkhTyQ+h2yNKhlROAaDllWgRyePAUitC08sXw26Eb6aw==", - "hasInstallScript": true, - "dependencies": { - "nan": "^2.14.0", - "node-pre-gyp": "^0.15.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dependencies": { - "traverse": ">=0.3.0 <0.4" - } - }, - "node_modules/chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/chart.js": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz", - "integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==", - "dependencies": { - "chartjs-color": "^2.1.0", - "moment": "^2.10.2" - } - }, - "node_modules/chartjs-color": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", - "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", - "dependencies": { - "chartjs-color-string": "^0.6.0", - "color-convert": "^1.9.3" - } - }, - "node_modules/chartjs-color-string": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", - "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", - "dependencies": { - "color-name": "^1.0.0" - } - }, - "node_modules/chartjs-color/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/chartjs-color/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/chartjs-node-canvas": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/chartjs-node-canvas/-/chartjs-node-canvas-3.1.0.tgz", - "integrity": "sha512-yL5hdb8dnhknBuUkz4CGL/rFSNudd8eKmKUsgVfrK5Fy8VswDI+DEfT4t2L/8H8sMRkzpAl42pEbERIghv3b0w==", - "dependencies": { - "canvas": "^2.6.1", - "tslib": "^1.14.1" - } - }, - "node_modules/cheerio": { - "version": "1.0.0-rc.6", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.6.tgz", - "integrity": "sha512-hjx1XE1M/D5pAtMgvWwE21QClmAEeGHOIDfycgmndisdNgI6PE1cGRQkMGBcsbUbmEQyWu5PJLUcAOjtQS8DWw==", - "dependencies": { - "cheerio-select": "^1.3.0", - "dom-serializer": "^1.3.1", - "domhandler": "^4.1.0", - "htmlparser2": "^6.1.0", - "parse5": "^6.0.1", - "parse5-htmlparser2-tree-adapter": "^6.0.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/cheerio-select": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.3.0.tgz", - "integrity": "sha512-mLgqdHxVOQyhOIkG5QnRkDg7h817Dkf0dAvlCio2TJMmR72cJKH0bF28SHXvLkVrGcGOiub0/Bs/CMnPeQO7qw==", - "dependencies": { - "css-select": "^4.0.0", - "css-what": "^5.0.0", - "domelementtype": "^2.2.0", - "domhandler": "^4.1.0", - "domutils": "^2.5.2" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/complex.js": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.11.tgz", - "integrity": "sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw==", - "engines": { - "node": "*" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "node_modules/css-select": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.0.0.tgz", - "integrity": "sha512-I7favumBlDP/nuHBKLfL5RqvlvRdn/W29evvWJ+TaoGPm7QD+xSIN5eY2dyGjtkUmemh02TZrqJb4B8DWo6PoQ==", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^5.0.0", - "domhandler": "^4.1.0", - "domutils": "^2.5.1", - "nth-check": "^2.0.0" - } - }, - "node_modules/css-what": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.0.tgz", - "integrity": "sha512-qxyKHQvgKwzwDWC/rGbT821eJalfupxYW2qbSJSAtdSTimsr/MlaGONoNLllaUPZWf8QnbcKM/kPVYUQuEKAFA==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/decimal.js": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", - "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==" - }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/discord.js": { - "version": "12.5.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz", - "integrity": "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==", - "dependencies": { - "@discordjs/collection": "^0.1.6", - "@discordjs/form-data": "^3.0.1", - "abort-controller": "^3.0.0", - "node-fetch": "^2.6.1", - "prism-media": "^1.2.9", - "setimmediate": "^1.0.5", - "tweetnacl": "^1.0.3", - "ws": "^7.4.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/diskusage": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/diskusage/-/diskusage-1.1.3.tgz", - "integrity": "sha512-EAyaxl8hy4Ph07kzlzGTfpbZMNAAAHXSZtNEMwdlnSd1noHzvA6HsgKt4fEMSvaEXQYLSphe5rPMxN4WOj0hcQ==", - "dependencies": { - "es6-promise": "^4.2.5", - "nan": "^2.14.0" - } - }, - "node_modules/dom-serializer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.1.tgz", - "integrity": "sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q==", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "entities": "^2.0.0" - } - }, - "node_modules/dom-walk": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" - }, - "node_modules/domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" - }, - "node_modules/domhandler": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.1.0.tgz", - "integrity": "sha512-/6/kmsGlMY4Tup/nGVutdrK9yQi4YjWVcVeoQmixpzjOUK1U7pQkvAPHBJeUxOgxF0J8f8lwCJSlCfD0V4CMGQ==", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/domutils": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.5.2.tgz", - "integrity": "sha512-MHTthCb1zj8f1GVfRpeZUbohQf/HdBos0oX5gZcQFepOZPLLRyj6Wn7XS7EMnY7CVpwv8863u2vyE83Hfu28HQ==", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.1.0" - } - }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "node_modules/escape-latex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", - "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/exif-parser": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", - "integrity": "sha1-WKnS1ywCwfbwKg70qRZicrd2CSI=" - }, - "node_modules/ffmpeg-static": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-4.3.0.tgz", - "integrity": "sha512-w/tXYGlOSeAkPHjypjzylaChLrG5wRzHFyB47KFRDsGyBxUJJWiq9I/39/e6r9Y4aY1gzpejTLg5Aa0aqb0XXA==", - "dependencies": { - "@derhuerst/http-basic": "^8.2.0", - "env-paths": "^2.2.0", - "https-proxy-agent": "^5.0.0", - "progress": "^2.0.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ffmpeg-static/node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/file-type": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz", - "integrity": "sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/follow-redirects": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", - "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/fraction.js": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.13.tgz", - "integrity": "sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA==", - "engines": { - "node": "*" - } - }, - "node_modules/fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dependencies": { - "minipass": "^2.6.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/gifwrap": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.2.tgz", - "integrity": "sha512-fcIswrPaiCDAyO8xnWvHSZdWChjKXUanKKpAiWWJ/UTkEi/aYKn5+90e7DE820zbEaVR9CE2y4z9bzhQijZ0BA==", - "dependencies": { - "image-q": "^1.1.1", - "omggif": "^1.0.10" - } - }, - "node_modules/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dependencies": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/http-response-object": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", - "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", - "dependencies": { - "@types/node": "^10.0.3" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "node_modules/ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "dependencies": { - "minimatch": "^3.0.4" - } - }, - "node_modules/image-q": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/image-q/-/image-q-1.1.1.tgz", - "integrity": "sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY=", - "engines": { - "node": ">=0.9.0" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/int64-buffer": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", - "integrity": "sha1-J3siiofZWtd30HwTgyAiQGpHNCM=" - }, - "node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-function": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", - "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==" - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "node_modules/javascript-natural-sort": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", - "integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=" - }, - "node_modules/jimp": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.16.1.tgz", - "integrity": "sha512-+EKVxbR36Td7Hfd23wKGIeEyHbxShZDX6L8uJkgVW3ESA9GiTEPK08tG1XI2r/0w5Ch0HyJF5kPqF9K7EmGjaw==", - "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/custom": "^0.16.1", - "@jimp/plugins": "^0.16.1", - "@jimp/types": "^0.16.1", - "regenerator-runtime": "^0.13.3" - } - }, - "node_modules/jpeg-js": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.2.tgz", - "integrity": "sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw==" - }, - "node_modules/leb": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/leb/-/leb-0.3.0.tgz", - "integrity": "sha1-Mr7p+tFoMo1q6oUi2DP0GA7tHaM=" - }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" - }, - "node_modules/load-bmfont": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz", - "integrity": "sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA==", - "dependencies": { - "buffer-equal": "0.0.1", - "mime": "^1.3.4", - "parse-bmfont-ascii": "^1.0.3", - "parse-bmfont-binary": "^1.0.5", - "parse-bmfont-xml": "^1.1.4", - "phin": "^2.9.1", - "xhr": "^2.0.1", - "xtend": "^4.0.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.toarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", - "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/luxon": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.26.0.tgz", - "integrity": "sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A==", - "engines": { - "node": "*" - } - }, - "node_modules/lzma": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/lzma/-/lzma-2.3.2.tgz", - "integrity": "sha1-N4OySFi5wOdHoN88vx+1/KqSxEE=", - "bin": { - "lzma.js": "bin/lzma.js" - } - }, - "node_modules/lzma-native": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-7.0.1.tgz", - "integrity": "sha512-+5hYbtTWijMtGg3HAn3VZ/XMP2tQ5nPik5i7csWPsXUHSOOFQcWlEwfjS/2ke0j6KO64oYUNr7SOqU5XoclBNw==", - "hasInstallScript": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.1", - "node-addon-api": "^3.1.0", - "readable-stream": "^3.6.0", - "rimraf": "^3.0.2" - }, - "bin": { - "lzmajs": "bin/lzmajs" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/lzma-native/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/lzma-native/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/mathjs": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-9.3.0.tgz", - "integrity": "sha512-0kYW+TXgB8lCqUj5wHR2hqAO2twSbPRelSFgRJXiwAx4nM6FrIb43Jd6XhW7sVbwYB+9HCNiyg5Kn8VYeB7ilg==", - "dependencies": { - "complex.js": "^2.0.11", - "decimal.js": "^10.2.1", - "escape-latex": "^1.2.0", - "fraction.js": "^4.0.13", - "javascript-natural-sort": "^0.7.1", - "seedrandom": "^3.0.5", - "tiny-emitter": "^2.1.0", - "typed-function": "^2.0.0" - }, - "bin": { - "mathjs": "bin/cli.js" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "bin": { - "mime": "cli.js" - } - }, - "node_modules/mime-db": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", - "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.29", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", - "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", - "dependencies": { - "mime-db": "1.46.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", - "dependencies": { - "dom-walk": "^0.1.0" - } - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "node_modules/minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dependencies": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "node_modules/minipass/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dependencies": { - "minipass": "^2.9.0" - } - }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", - "engines": { - "node": "*" - } - }, - "node_modules/nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" - }, - "node_modules/needle": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", - "integrity": "sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==", - "dependencies": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 4.4.x" - } - }, - "node_modules/needle/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/needle/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/node-addon-api": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", - "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==" - }, - "node_modules/node-emoji": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", - "integrity": "sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw==", - "dependencies": { - "lodash.toarray": "^4.4.0" - } - }, - "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "engines": { - "node": "4.x || >=6.0.0" - } - }, - "node_modules/node-localstorage": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.1.6.tgz", - "integrity": "sha512-yE7AycE5G2hU55d+F7Ona9nx97C+enJzWWx6jrsji7fuPZFJOvuW3X/LKKAcXRBcEIJPDOKt8ZiFWFmShR/irg==", - "dependencies": { - "write-file-atomic": "^1.1.4" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/node-osr": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/node-osr/-/node-osr-1.2.1.tgz", - "integrity": "sha512-BKTE4Nx4Mim9/Q5i8mdTxw7Cp8ljtLg93EFMUvP7xsfxgruqA50MKyPj9a9SM6utyl1+HFpdmtvaAf3cp3G1mg==", - "dependencies": { - "int64-buffer": "^0.1.9", - "leb": "^0.3.0", - "lzma": "^2.3.2" - } - }, - "node_modules/node-pre-gyp": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz", - "integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==", - "dependencies": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.3", - "needle": "^2.5.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/node-pre-gyp/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "dependencies": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" - }, - "node_modules/npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "dependencies": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "node_modules/nth-check": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", - "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", - "dependencies": { - "boolbase": "^1.0.0" - } - }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-path": { - "version": "0.11.5", - "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.5.tgz", - "integrity": "sha512-jgSbThcoR/s+XumvGMTMf81QVBmah+/Q7K7YduKeKVWL7N111unR2d6pZZarSk6kY/caeNxUDyxOvMWyzoU2eg==", - "engines": { - "node": ">= 10.12.0" - } - }, - "node_modules/ojsama": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ojsama/-/ojsama-2.2.0.tgz", - "integrity": "sha512-kO0IaYP+u1QyMrajfm6m6a0xKNCf7psD/CjEHpLZ7gOx4YRJM6m2fEyNZd187Ocrlb+gOqbITDMyLpvOsqzsCg==" - }, - "node_modules/omggif": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", - "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/osu-parser": { - "resolved": "git+ssh://git@github.com/LeaPhant/osu-parser.git#e061a68aecfcdca01de3a1bdbb5f604d5e4ff6f0" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/parse-bmfont-ascii": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", - "integrity": "sha1-Eaw8P/WPfCAgqyJ2kHkQjU36AoU=" - }, - "node_modules/parse-bmfont-binary": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", - "integrity": "sha1-0Di0dtPp3Z2x4RoLDlOiJ5K2kAY=" - }, - "node_modules/parse-bmfont-xml": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.4.tgz", - "integrity": "sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ==", - "dependencies": { - "xml-parse-from-string": "^1.0.0", - "xml2js": "^0.4.5" - } - }, - "node_modules/parse-cache-control": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", - "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=" - }, - "node_modules/parse-headers": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz", - "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA==" - }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", - "dependencies": { - "parse5": "^6.0.1" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/phin": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/phin/-/phin-2.9.3.tgz", - "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==" - }, - "node_modules/pixelmatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", - "integrity": "sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ=", - "dependencies": { - "pngjs": "^3.0.0" - }, - "bin": { - "pixelmatch": "bin/pixelmatch" - } - }, - "node_modules/pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/prism-media": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.9.tgz", - "integrity": "sha512-UHCYuqHipbTR1ZsXr5eg4JUmHER8Ss4YEb9Azn+9zzJ7/jlTtD1h0lc4g6tNx3eMlB8Mp6bfll0LPMAV4R6r3Q==" - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readline-sync": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", - "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "node_modules/seedrandom": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", - "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" - }, - "node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, - "node_modules/signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" - }, - "node_modules/simple-get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", - "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", - "engines": { - "node": "*" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "dependencies": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "engines": { - "node": ">=4.5" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/timm": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", - "integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==" - }, - "node_modules/tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" - }, - "node_modules/tinycolor2": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", - "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==", - "engines": { - "node": "*" - } - }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/tweetnacl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", - "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" - }, - "node_modules/typed-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-2.0.0.tgz", - "integrity": "sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "node_modules/tz-lookup": { - "version": "6.1.25", - "resolved": "https://registry.npmjs.org/tz-lookup/-/tz-lookup-6.1.25.tgz", - "integrity": "sha512-fFewT9o1uDzsW1QnUU1ValqaihFnwiUiiHr1S79/fxOzKXYYvX+EHeRnpvQJ9B3Qg67wPXT6QF2Esc4pFOrvLg==" - }, - "node_modules/unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/utif": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz", - "integrity": "sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==", - "dependencies": { - "pako": "^1.0.5" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/vm2": { - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.3.tgz", - "integrity": "sha512-smLS+18RjXYMl9joyJxMNI9l4w7biW8ilSDaVRvFBDwOH8P0BK1ognFQTpg0wyQ6wIKLTblHJvROW692L/E53Q==", - "bin": { - "vm2": "bin/vm2" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", - "dependencies": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "slide": "^1.1.5" - } - }, - "node_modules/ws": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", - "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/xhr": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", - "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", - "dependencies": { - "global": "~4.4.0", - "is-function": "^1.0.1", - "parse-headers": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "node_modules/xml-parse-from-string": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", - "integrity": "sha1-qQKekp09vN7RafPG4oI42VpdWig=" - }, - "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, "dependencies": { "@babel/runtime": { "version": "7.13.10", @@ -3204,6 +895,11 @@ "readable-stream": "^2.0.2" } }, + "easy-stack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==" + }, "entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -3224,6 +920,11 @@ "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" }, + "event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==" + }, "event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -3490,6 +1191,19 @@ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.2.tgz", "integrity": "sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw==" }, + "js-message": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==" + }, + "js-queue": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz", + "integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==", + "requires": { + "easy-stack": "^1.0.1" + } + }, "leb": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/leb/-/leb-0.3.0.tgz", @@ -3733,6 +1447,16 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, + "node-ipc": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz", + "integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==", + "requires": { + "event-pubsub": "4.3.0", + "js-message": "1.0.7", + "js-queue": "2.0.2" + } + }, "node-localstorage": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.1.6.tgz", @@ -4073,14 +1797,6 @@ "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -4091,6 +1807,14 @@ "strip-ansi": "^3.0.0" } }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", diff --git a/package.json b/package.json index f3e2691..51ffea7 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "scripts": { "start": "git pull; npm i && [ ! -f ./config.json ] && npm run config; node index", + "fast": "node index", "test": "echo \"Error: no test specified\" && exit 1", "commands": "node generate-commands-md", "config": "node generate-config", @@ -33,6 +34,7 @@ "mathjs": "^9.3.0", "node-emoji": "^1.10.0", "node-fetch": "^2.6.1", + "node-ipc": "^9.2.1", "node-localstorage": "^2.1.6", "node-osr": "^1.2.1", "object-path": "^0.11.5", diff --git a/renderer/render_frame.js b/renderer/render_frame.js index f0476a2..63555b8 100644 --- a/renderer/render_frame.js +++ b/renderer/render_frame.js @@ -1,3 +1,8 @@ +// noinspection JSUnresolvedVariable,JSUnresolvedFunction,JSVoidFunctionReturnValueUsed +// noinspection JSUnresolvedVariable + +const ipc = require('node-ipc'); + const fs = require('fs'); const path = require('path'); const os = require('os'); @@ -11,13 +16,20 @@ const ffmpeg = require('ffmpeg-static'); const unzip = require('unzipper'); const disk = require('diskusage'); -const { execFile, fork, spawn } = require('child_process'); +const {execFile, fork, spawn} = require('child_process'); const config = require('../config.json'); const helper = require('../helper.js'); const MAX_SIZE = 8 * 1024 * 1024; + + +let frame_counter = 0; + + + + let enabled_mods = [""]; const resources = path.resolve(__dirname, "res"); @@ -60,31 +72,31 @@ const default_hitsounds = [ "drum-sliderwhistle", ]; -function getTimingPoint(timingPoints, offset){ - let timingPoint = timingPoints[0]; +function getTimingPoint(timingPoints, offset) { + let timingPoint = timingPoints[0]; - for(let x = timingPoints.length - 1; x >= 0; x--){ - if(timingPoints[x].offset <= offset){ - timingPoint = timingPoints[x]; - break; - } - } + for (let x = timingPoints.length - 1; x >= 0; x--) { + if (timingPoints[x].offset <= offset) { + timingPoint = timingPoints[x]; + break; + } + } - return timingPoint; + return timingPoint; } -async function processHitsounds(beatmap_path){ +async function processHitsounds(beatmap_path) { let hitSoundPath = {}; let setHitSound = (file, base_path, custom) => { let hitSoundName = path.basename(file, path.extname(file)); - if(hitSoundName.match(/\d+/) === null && custom) + if (hitSoundName.match(/\d+/) === null && custom) hitSoundName += '1'; let absolutePath = path.resolve(base_path, file); - if(path.extname(file) === '.wav' || path.extname(file) === '.mp3') + if (path.extname(file) === '.wav' || path.extname(file) === '.mp3') hitSoundPath[hitSoundName] = absolutePath; }; @@ -103,25 +115,26 @@ async function processHitsounds(beatmap_path){ return hitSoundPath; } -async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, modded_length, time_scale, file_path){ +async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, modded_length, time_scale, file_path) { let media = await mediaPromise; - if(!media) + if (!media) throw "Beatmap data not available"; let execFilePromise = util.promisify(execFile); let beatmapAudio = false; - try{ + try { + helper.log(start_time) await execFilePromise(ffmpeg, [ '-ss', start_time / 1000, '-i', `"${media.audio_path}"`, '-t', actual_length * Math.max(1, time_scale) / 1000, '-filter:a', `"afade=t=out:st=${Math.max(0, actual_length * time_scale / 1000 - 0.5 / time_scale)}:d=0.5,atempo=${time_scale},volume=0.7"`, path.resolve(file_path, 'audio.wav') - ], { shell: true }); + ], {shell: true}); beatmapAudio = true; - }catch(e){ + } catch (e) { console.error(e); //throw "Error trimming beatmap audio"; } @@ -133,11 +146,11 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, const scoringFrames = beatmap.ScoringFrames.filter(a => a.offset >= start_time && a.offset < start_time + actual_length); - if(beatmap.Replay.auto !== true){ - for(const scoringFrame of scoringFrames){ - if(scoringFrame.combo >= scoringFrame.previousCombo || scoringFrame.previousCombo < 30) + if (beatmap.Replay.auto !== true) { + for (const scoringFrame of scoringFrames) { + if (scoringFrame.combo >= scoringFrame.previousCombo || scoringFrame.previousCombo < 30) continue; - + hitSounds.push({ offset: (scoringFrame.offset - start_time) / time_scale, sound: 'combobreak', @@ -147,25 +160,25 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, } } - for(const hitObject of hitObjects){ + for (const hitObject of hitObjects) { let timingPoint = getTimingPoint(beatmap.timingPoints, hitObject.startTime); - if(hitObject.objectName == 'circle' && Array.isArray(hitObject.HitSounds)){ + if (hitObject.objectName == 'circle' && Array.isArray(hitObject.HitSounds)) { let offset = hitObject.startTime; - if(beatmap.Replay.auto !== true){ - if(hitObject.hitOffset == null) + if (beatmap.Replay.auto !== true) { + if (hitObject.hitOffset == null) continue; - + offset += hitObject.hitOffset; } offset -= start_time; offset /= time_scale; - for(const hitSound of hitObject.HitSounds){ + for (const hitSound of hitObject.HitSounds) { - if(hitSound in hitSoundPaths){ + if (hitSound in hitSoundPaths) { hitSounds.push({ offset, sound: hitSound, @@ -175,16 +188,16 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, } } } - - if(hitObject.objectName == 'slider'){ + + if (hitObject.objectName == 'slider') { hitObject.EdgeHitSounds.forEach((edgeHitSounds, index) => { edgeHitSounds.forEach(hitSound => { let offset = hitObject.startTime + index * (hitObject.duration / hitObject.repeatCount); - if(index == 0 && beatmap.Replay.auto !== true){ - if(hitObject.hitOffset == null) + if (index == 0 && beatmap.Replay.auto !== true) { + if (hitObject.hitOffset == null) return; - + offset += hitObject.hitOffset; } @@ -193,7 +206,7 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, offset -= start_time; offset /= time_scale; - if(hitSound in hitSoundPaths){ + if (hitSound in hitSoundPaths) { hitSounds.push({ offset, sound: hitSound, @@ -205,7 +218,7 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, }); hitObject.SliderTicks.forEach(tick => { - for(let i = 0; i < hitObject.repeatCount; i++){ + for (let i = 0; i < hitObject.repeatCount; i++) { let offset = hitObject.startTime + (i % 2 == 0 ? tick.offset : tick.reverseOffset) + i * (hitObject.duration / hitObject.repeatCount); let tickTimingPoint = getTimingPoint(beatmap.timingPoints, offset); @@ -214,7 +227,7 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, offset /= time_scale; tick.HitSounds[i].forEach(hitSound => { - if(hitSound in hitSoundPaths){ + if (hitSound in hitSoundPaths) { hitSounds.push({ type: 'slidertick', offset, @@ -234,7 +247,7 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, let hitSoundIndexes = {}; hitSounds.forEach(hitSound => { - if(!(hitSound.sound in hitSoundIndexes)){ + if (!(hitSound.sound in hitSoundIndexes)) { ffmpegArgs.push('-guess_layout_max', '0', '-i', hitSound.path); hitSoundIndexes[hitSound.sound] = Object.keys(hitSoundIndexes).length; } @@ -247,10 +260,10 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, let mergeHitSoundArgs = []; let chunksToMerge = 0; - for(let i = 0; i < chunkCount; i++){ + for (let i = 0; i < chunkCount; i++) { let hitSoundsChunk = hitSounds.filter(a => a.offset >= i * chunkLength && a.offset < (i + 1) * chunkLength); - if(hitSoundsChunk.length == 0) + if (hitSoundsChunk.length === 0) continue; chunksToMerge++; @@ -278,11 +291,11 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, ffmpegArgsChunk.push(`"${filterComplex}"`, '-ac', '2', path.resolve(file_path, `hitsounds${i}.wav`)); mergeHitSoundArgs.push('-guess_layout_max', '0', '-i', path.resolve(file_path, `hitsounds${i}.wav`)); - hitSoundPromises.push(execFilePromise(ffmpeg, ffmpegArgsChunk, { shell: true })); + hitSoundPromises.push(execFilePromise(ffmpeg, ffmpegArgsChunk, {shell: true})); } return new Promise((resolve, reject) => { - if(chunksToMerge < 1){ + if (chunksToMerge < 1) { reject(); return; } @@ -290,12 +303,12 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, Promise.all(hitSoundPromises).then(async () => { mergeHitSoundArgs.push('-filter_complex', `amix=inputs=${chunksToMerge}:dropout_transition=${actual_length},volume=${chunksToMerge},dynaudnorm`, path.resolve(file_path, `hitsounds.wav`)); - await execFilePromise(ffmpeg, mergeHitSoundArgs, { shell: true }); + await execFilePromise(ffmpeg, mergeHitSoundArgs, {shell: true}); const mergeArgs = []; let mixInputs = beatmapAudio ? 2 : 1; - if(beatmapAudio) + if (beatmapAudio) mergeArgs.push('-guess_layout_max', '0', '-i', path.resolve(file_path, `audio.wav`)); mergeArgs.push( @@ -303,31 +316,33 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, '-filter_complex', `amix=inputs=${mixInputs}:duration=first:dropout_transition=${actual_length},volume=2,dynaudnorm`, path.resolve(file_path, 'merged.wav') ); - await execFilePromise(ffmpeg, mergeArgs, { shell: true }); + await execFilePromise(ffmpeg, mergeArgs, {shell: true}); resolve(path.resolve(file_path, 'merged.wav')); }); }); } -async function downloadMedia(options, beatmap, beatmap_path, size, download_path){ - if(options.type != 'mp4' || !options.audio || !config.credentials.osu_api_key) +async function downloadMedia(options, beatmap, beatmap_path, size, download_path) { + if (options.type !== 'mp4' || !options.audio || !config.credentials.osu_api_key) return false; let output = {}; let beatmapset_id = beatmap.BeatmapSetID; - if(beatmapset_id == null){ + if (beatmapset_id == null) { const content = await fs.promises.readFile(beatmap_path, 'utf8'); const hash = crypto.createHash('md5').update(content).digest("hex"); - const { data } = await axios.get('https://osu.ppy.sh/api/get_beatmaps', { params: { - k: config.credentials.osu_api_key, - h: hash - }}); + const {data} = await axios.get('https://osu.ppy.sh/api/get_beatmaps', { + params: { + k: config.credentials.osu_api_key, + h: hash + } + }); - if(data.length == 0){ + if (data.length == 0) { throw "Couldn't find beatmap"; } @@ -336,23 +351,26 @@ async function downloadMedia(options, beatmap, beatmap_path, size, download_path let mapStream; - try{ - const chimuCheckMapExists = await axios.get(`https://api.chimu.moe/v1/set/${beatmapset_id}`, { timeout: 2000 }); + try { + const chimuCheckMapExists = await axios.get(`https://api.chimu.moe/v1/set/${beatmapset_id}`, {timeout: 2000}); - if(chimuCheckMapExists.status != 200) + if (chimuCheckMapExists.status !== 200) throw "Map not found"; - const chimuMap = await axios.get(`https://api.chimu.moe/v1/download/${beatmapset_id}?n=0`, { timeout: 10000, responseType: 'stream' }); + const chimuMap = await axios.get(`https://api.chimu.moe/v1/download/${beatmapset_id}?n=0`, { + timeout: 10000, + responseType: 'stream' + }); mapStream = chimuMap.data; - }catch(e){ - const beatconnectMap = await axios.get(`https://beatconnect.io/b/${beatmapset_id}`, { responseType: 'stream' }); + } catch (e) { + const beatconnectMap = await axios.get(`https://beatconnect.io/b/${beatmapset_id}`, {responseType: 'stream'}); mapStream = beatconnectMap.data; } const extraction_path = path.resolve(download_path, 'map'); - const extraction = mapStream.pipe(unzip.Extract({ path: extraction_path })); + const extraction = mapStream.pipe(unzip.Extract({path: extraction_path})); await new Promise((resolve, reject) => { extraction.on('close', resolve); @@ -361,29 +379,37 @@ async function downloadMedia(options, beatmap, beatmap_path, size, download_path output.beatmap_path = extraction_path; - if(beatmap.AudioFilename && await helper.fileExists(path.resolve(extraction_path, beatmap.AudioFilename))) + if (beatmap.AudioFilename && await helper.fileExists(path.resolve(extraction_path, beatmap.AudioFilename))) output.audio_path = path.resolve(extraction_path, beatmap.AudioFilename); - if(beatmap.bgFilename && await helper.fileExists(path.resolve(extraction_path, beatmap.bgFilename))) + if (beatmap.bgFilename + && await helper.fileExists(path.resolve(extraction_path, beatmap.bgFilename)) + && !options.nobg) { output.background_path = path.resolve(extraction_path, beatmap.bgFilename); + } - if(beatmap.bgFilename && output.background_path){ - try{ + if (beatmap.bgFilename && output.background_path) { + try { const img = await Jimp.read(output.background_path); + let bg_shade = 80; + if (options.bg_opacity) { + bg_shade = 100 - options.bg_opacity; + } + await img - .cover(...size) - .color([ - { apply: 'shade', params: [80] } - ]) - .writeAsync(path.resolve(extraction_path, 'bg.png')); - + .cover(...size) + .color([ + {apply: 'shade', params: [bg_shade]} + ]) + .writeAsync(path.resolve(extraction_path, 'bg.png')); + output.background_path = path.resolve(extraction_path, 'bg.png'); - }catch(e){ + } catch (e) { output.background_path = null; helper.error(e); } - }else if(Object.keys(output).length == 0){ + } else if (Object.keys(output).length == 0) { return false; } @@ -393,64 +419,74 @@ async function downloadMedia(options, beatmap, beatmap_path, size, download_path let beatmap, speed_multiplier; module.exports = { - get_frame: function(beatmap_path, time, enabled_mods, size, options, cb){ - let worker = fork(path.resolve(__dirname, 'beatmap_preprocessor.js'), ['--max-old-space-size=512']); + get_frame: function (beatmap_path, time, enabled_mods, size, options, cb) { + let worker = fork(path.resolve(__dirname, 'beatmap_preprocessor.js'), ['--max-old-space-size=512']); - worker.send({ - beatmap_path, - options, - enabled_mods - }); + worker.send({ + beatmap_path, + options, + enabled_mods + }); worker.on('close', code => { - if(code > 0){ + if (code > 0) { cb("Error processing beatmap"); return false; } }); - worker.on('message', _beatmap => { - beatmap = _beatmap; + worker.on('message', _beatmap => { + beatmap = _beatmap; - if(time == 0 && options.percent){ - time = beatmap.hitObjects[Math.floor(options.percent * beatmap.hitObjects.length)].startTime - 2000; - }else{ - let firstNonSpinner = beatmap.hitObjects.filter(x => x.objectName != 'spinner'); - time = Math.max(time, firstNonSpinner[0].startTime); - } + if (time === 0 && options.percent) { + time = beatmap.hitObjects[Math.floor(options.percent * beatmap.hitObjects.length)].startTime - 2000; + } else { + let firstNonSpinner = beatmap.hitObjects.filter(x => x.objectName !== 'spinner'); + time = Math.max(time, firstNonSpinner[0].startTime); + } - let worker = fork(path.resolve(__dirname, 'render_worker.js')); + let worker = fork(path.resolve(__dirname, 'render_worker.js')); worker.on('close', code => { - if(code > 0){ + if (code > 0) { cb("Error rendering beatmap"); return false; } }); - worker.on('message', buffer => { - cb(null, Buffer.from(buffer, 'base64')); - }); + worker.on('message', buffer => { + cb(null, Buffer.from(buffer, 'base64')); + }); - worker.send({ - beatmap, - start_time: time, - options, - size - }); - }); - }, + worker.send({ + beatmap, + start_time: time, + options, + size + }); + }); + }, - get_frames: async function(beatmap_path, time, length, enabled_mods, size, options, cb){ - if(config.debug) - console.time('process beatmap'); + get_frames: async function (beatmap_path, time, length, enabled_mods, size, options, cb) { - const { msg } = options; + + + if (config.debug) + console.time('process beatmap'); + + function Queue() { this.frames = []; } + Queue.prototype.enqueue = function (frame){ this.frames.push(frame); } + Queue.prototype.dequeue = function () { return this.frames.shift(); } + Queue.prototype.isEmpty = function () { return this.frames.length === 0; } + Queue.prototype.peek = function () { return !this.isEmpty() ? this.frames[0] : undefined; } + + const {msg} = options; options.msg = null; const renderStatus = ['– processing beatmap', '– rendering frames', '– encoding video']; + // noinspection JSCheckFunctionSignatures const renderMessage = await msg.channel.send({embed: {description: renderStatus.join("\n")}}); const updateRenderStatus = async () => { @@ -461,7 +497,9 @@ module.exports = { }); }; - const updateInterval = setInterval(() => { updateRenderStatus().catch(console.error) }, 3000); + const updateInterval = setInterval(() => { + updateRenderStatus().catch(console.error) + }, 3000); updateRenderStatus().catch(console.error); @@ -475,19 +513,19 @@ module.exports = { const beatmapProcessStart = Date.now(); - let worker = fork(path.resolve(__dirname, 'beatmap_preprocessor.js')); + let worker = fork(path.resolve(__dirname, 'beatmap_preprocessor.js')); let frames_rendered = [], frames_piped = [], current_frame = 0; - worker.send({ - beatmap_path, - options, - enabled_mods, + worker.send({ + beatmap_path, + options, + enabled_mods, speed_override: options.speed - }); + }); worker.on('close', code => { - if(code > 0){ + if (code > 0) { resolveRender("Error processing beatmap").catch(console.error); return false; @@ -496,143 +534,151 @@ module.exports = { renderStatus[0] = `✓ processing beatmap (${((Date.now() - beatmapProcessStart) / 1000).toFixed(3)}s)`; }); - worker.on('message', async _beatmap => { - beatmap = _beatmap; - if(config.debug) - console.timeEnd('process beatmap'); - if(time == 0 && options.percent){ - time = beatmap.hitObjects[Math.floor(options.percent * (beatmap.hitObjects.length - 1))].startTime - 2000; - }else if(options.objects){ - let objectIndex = 0; + let worker_frame_queues = {}; + let worker_frame_buffers = {}; - for(let i = 0; i < beatmap.hitObjects.length; i++){ - if(beatmap.hitObjects[i].startTime >= time){ - objectIndex = i; - break; - } - } - time -= 200; - if(beatmap.hitObjects.length > objectIndex + options.objects) - length = beatmap.hitObjects[objectIndex + options.objects].startTime - time + 400; + worker.on('message', async _beatmap => { + beatmap = _beatmap; + beatmap.hitObjects = _beatmap.hitObjects; // to make IDE shut up about unknown reference - if(length >= 10 * 1000) - options.type = 'mp4'; + if (config.debug) + console.timeEnd('process beatmap'); - }else{ - let firstNonSpinner = beatmap.hitObjects.filter(x => x.objectName != 'spinner'); - time = Math.max(time, Math.max(0, firstNonSpinner[0].startTime - 1000)); - } + { + if (time === 0 && options.percent) { + time = beatmap.hitObjects[Math.floor(options.percent * (beatmap.hitObjects.length - 1))].startTime - 2000; + if (options.offset) { + time += options.offset + } + } else if (options.objects) { + let objectIndex = 0; - if(options.combo){ + for (let i = 0; i < beatmap.hitObjects.length; i++) { + if (beatmap.hitObjects[i].startTime >= time) { + objectIndex = i; + break; + } + } + + if (options.offset) { + time += options.offset + helper.log(options.offset) + } + time -= 200; + + if (beatmap.hitObjects.length > objectIndex + options.objects) + length = beatmap.hitObjects[objectIndex + options.objects].startTime - time + 400; + + if (length >= 10 * 1000) + options.type = 'mp4'; + + } else { + let firstNonSpinner = beatmap.hitObjects.filter(x => x.objectName != 'spinner'); + time = Math.max(time, Math.max(0, firstNonSpinner[0].startTime - 1000)); + if (options.offset) { + time += options.offset + helper.log(options.offset) + } + } + } + + if (options.combo) { let current_combo = 0; - for(let hitObject of beatmap.hitObjects){ - if(hitObject.objectName == 'slider'){ + for (let hitObject of beatmap.hitObjects) { + if (hitObject.objectName === 'slider') { current_combo += 1; - for(let i = 0; i < hitObject.repeatCount; i++){ + for (let i = 0; i < hitObject.repeatCount; i++) { current_combo += 1 + hitObject.SliderTicks.length; time = hitObject.startTime + i * (hitObject.duration / hitObject.repeatCount); - if(current_combo >= options.combo) + if (current_combo >= options.combo) break; } - if(current_combo >= options.combo) + if (current_combo >= options.combo) break; - }else{ + } else { current_combo += 1; time = hitObject.endTime; - if(current_combo >= options.combo) + if (current_combo >= options.combo) break; } } } let lastObject = beatmap.hitObjects[beatmap.hitObjects.length - 1]; - let lastObjectTime = lastObject.endTime + 1500; - length = Math.min(400 * 1000, length); - - let start_time = time; - - let time_max = Math.min(time + length + 1000, lastObjectTime); + length = Math.min(900 * 1000, length); + let start_time = time; + let time_max = Math.min(time + length + 1000, lastObjectTime); + let actual_length = time_max - time; - let actual_length = time_max - time; + let rnd = Math.round(1e9 * Math.random()); + let file_path; + let fps = options.fps || 60; - let rnd = Math.round(1e9 * Math.random()); - let file_path; - let fps = options.fps || 60; + let time_scale = 1; - let i = 0; + if (enabled_mods.includes('DT') || enabled_mods.includes('NC')) + time_scale *= 1.5; - let time_scale = 1; + if (enabled_mods.includes('HT') || enabled_mods.includes('DC')) + time_scale *= 0.75; - if(enabled_mods.includes('DT') || enabled_mods.includes('NC')) - time_scale *= 1.5; - - if(enabled_mods.includes('HT') || enabled_mods.includes('DC')) - time_scale *= 0.75; - - if(options.speed != 1) + if (options.speed !== 1) time_scale = options.speed; actual_length = Math.min(length + 1000, Math.max(actual_length, actual_length / time_scale)); - if(!('type' in options)) - options.type = 'gif'; - - if(options.type == 'gif') - fps = 50; - - let time_frame = 1000 / fps * time_scale; - - let bitrate = 500 * 1024; + if (!('type' in options)) { + options.type = 'gif'; + } + if (options.type === 'gif') { + fps = 50; + } - if(actual_length > 160 * 1000 && actual_length < 210 * 1000) - size = [350, 262]; - else if(actual_length >= 210 * 1000) - size = [180, 128]; + let time_frame = 1000 / fps * time_scale; + let bitrate = 500 * 1024; - if(actual_length > 360 * 1000){ - actual_length = 360 * 1000; - max_time = time + actual_length; - } - file_path = path.resolve(config.frame_path != null ? config.frame_path : os.tmpdir(), 'frames', `${rnd}`); + file_path = path.resolve(config.frame_path != null ? config.frame_path : os.tmpdir(), 'frames', `${rnd}`); - await fs.promises.mkdir(file_path, { recursive: true }); + await fs.promises.mkdir(file_path, {recursive: true}); let threads = require('os').cpus().length; + // let threads = 1; let modded_length = time_scale > 1 ? Math.min(actual_length * time_scale, lastObjectTime) : actual_length; let amount_frames = Math.floor(modded_length / time_frame); - let frames_size = amount_frames * size[0] * size[1] * 4; + // let frames_size = amount_frames * size[0] * size[1] * 4; + // recursively does reads frame from file and pipes to 2nd ffmpeg stdin let pipeFrameLoop = (ffmpegProcess, cb) => { - if(frames_rendered.includes(current_frame)){ + if (frames_rendered.includes(current_frame)) { let frame_path = path.resolve(file_path, `${current_frame}.rgba`); fs.promises.readFile(frame_path).then(buf => { ffmpegProcess.stdin.write(buf, err => { - if(err){ + if (err) { cb(null); return; } - fs.promises.rm(frame_path, { recursive: true }).catch(helper.error); + fs.promises.rm(frame_path, {recursive: true}).catch(helper.error); frames_piped.push(current_frame); frames_rendered.slice(frames_rendered.indexOf(current_frame), 1); - if(frames_piped.length == amount_frames){ + if (frames_piped.length === amount_frames) { ffmpegProcess.stdin.end(); cb(null); return; @@ -644,214 +690,315 @@ module.exports = { }).catch(err => { resolveRender("Error encoding video").catch(console.error); helper.error(err); - - return; }); - }else{ + } else { setTimeout(() => { pipeFrameLoop(ffmpegProcess, cb); - }, 100); + }, 50); } } - disk.check(file_path, (err, info) => { - if(err){ - helper.error(err); - cb(err); - return false; - } + let newPipeFrameLoop = async (ffmpegProcess, callback) => { + helper.log("hello? somebody there?") + let next_frame_worker_id = 0; + let frame_counter = 0; + while (frame_counter < amount_frames) { + helper.log("trying to feed frame " + frame_counter); + let next_frame_worker_queue = worker_frame_queues[next_frame_worker_id]; + let next_frame = next_frame_worker_queue.peek(); + if (typeof next_frame === 'undefined') { + await new Promise(r => setTimeout(r, 10)); + continue; + } + + // helper.log("in newPipeFrameLoop sending frames to ffmpeg") + // let next_frame_buffer = Buffer.from(next_frame); + let next_frame_buffer = Buffer.from(next_frame); + helper.log(next_frame_buffer.length); + ffmpegProcess.stdin.write(next_frame_buffer, err => { + if (err) { + helper.error("something bad happened"); + helper.error(err); + callback(null); + return; + } + // helper.log("in the callback, but no err"); + frame_counter++; - if(info.available * 0.9 < frames_size){ - resolveRender("Not enough disk space").catch(console.error); + if (frame_counter === amount_frames){ + ffmpegProcess.stdin.end(); + callback(null); + return; + } - return false; - } + next_frame_worker_id = (next_frame_worker_id + 1) % threads; + next_frame_worker_queue.dequeue(); + }); - let ffmpeg_args = [ - '-f', 'rawvideo', '-r', fps, '-s', size.join('x'), '-pix_fmt', 'rgba', - '-c:v', 'rawvideo', '-thread_queue_size', 1024, - '-i', 'pipe:0' - ]; + await new Promise(r => setTimeout(r, 5)); + } - let mediaPromise = downloadMedia(options, beatmap, beatmap_path, size, file_path); - let audioProcessingPromise = renderHitsounds(mediaPromise, beatmap, start_time, actual_length, modded_length, time_scale, file_path); + } - if(options.type == 'mp4') - bitrate = Math.min(bitrate, (0.7 * MAX_SIZE) * 8 / (actual_length / 1000) / 1024); - let workers = []; + let ffmpeg_args = ['-f', 'rawvideo', '-r', fps, '-s', size.join('x'), '-pix_fmt', 'rgba', + '-c:v', 'rawvideo', '-thread_queue_size', 1024, + '-i', 'pipe:0']; - for(let i = 0; i < threads; i++){ - workers.push( - fork(path.resolve(__dirname, 'render_worker.js')) - ); - } + let mediaPromise = downloadMedia(options, beatmap, beatmap_path, size, file_path); + let audioProcessingPromise = renderHitsounds(mediaPromise, beatmap, start_time, actual_length, modded_length, time_scale, file_path); - let done = 0; + if (options.type === 'mp4') + bitrate = Math.min(bitrate, (0.7 * MAX_SIZE) * 8 / (actual_length / 1000) / 1024); - if(config.debug) - console.time('render beatmap'); + let done = 0; - if(options.type == 'gif'){ - if(config.debug) - console.time('encode video'); + if (config.debug) + console.time('render beatmap'); - ffmpeg_args.push(`${file_path}/video.gif`); + if (options.type === 'gif') { + if (config.debug) + console.time('encode video'); - const encodingProcessStart = Date.now(); + ffmpeg_args.push(`${file_path}/video.gif`); - let ffmpegProcess = spawn(ffmpeg, ffmpeg_args, { shell: true }); + const encodingProcessStart = Date.now(); - ffmpegProcess.on('close', async code => { - if(code > 0){ - resolveRender("Error encoding video") + let ffmpegProcess = spawn(ffmpeg, ffmpeg_args, {shell: true}); + + ffmpegProcess.on('close', async code => { + if (code > 0) { + resolveRender("Error encoding video") .then(() => { - fs.promises.rm(file_path, { recursive: true }).catch(helper.error); + fs.promises.rm(file_path, {recursive: true}).catch(helper.error); }).catch(console.error); - return false; - } + return false; + } - if(config.debug) - console.timeEnd('encode video'); + if (config.debug) + console.timeEnd('encode video'); - renderStatus[1] = `✓ encoding video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; + renderStatus[1] = `✓ encoding video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; - resolveRender({files: [{ + resolveRender({ + files: [{ attachment: `${file_path}/video.${options.type}`, name: `video.${options.type}` - }]}).then(() => { - fs.promises.rm(file_path, { recursive: true }).catch(helper.error); - }).catch(console.error); - }); + }] + }).then(() => { + fs.promises.rm(file_path, {recursive: true}).catch(helper.error); + }).catch(console.error); + }); - pipeFrameLoop(ffmpegProcess, err => { - if(err){ - resolveRender("Error encoding video") + pipeFrameLoop(ffmpegProcess, err => { + if (err) { + resolveRender("Error encoding video") .then(() => { - fs.promises.rm(file_path, { recursive: true }).catch(helper.error); + fs.promises.rm(file_path, {recursive: true}).catch(helper.error); }).catch(console.error); - return false; - } - }); - }else{ - Promise.all([mediaPromise, audioProcessingPromise]).then(response => { - let media = response[0]; - let audio = response[1]; - - if(media.background_path) - ffmpeg_args.unshift('-loop', '1', '-r', fps, '-i', `"${media.background_path}"`); - else - ffmpeg_args.unshift('-f', 'lavfi', '-r', fps, '-i', `color=c=black:s=${size.join("x")}`); + return false; + } + }); + } else { + Promise.all([mediaPromise, audioProcessingPromise]).then(response => { + let media = response[0]; + let audio = response[1]; + + if (media.background_path) + ffmpeg_args.unshift('-loop', '1', '-r', fps, '-i', `"${media.background_path}"`); + else + ffmpeg_args.unshift('-f', 'lavfi', '-r', fps, '-i', `color=c=black:s=${size.join("x")}`); - ffmpeg_args.push('-i', audio); + ffmpeg_args.push('-i', audio); - bitrate -= 128; - }).catch(e => { - helper.error(e); - ffmpeg_args.unshift('-f', 'lavfi', '-r', fps, '-i', `color=c=black:s=${size.join("x")}`); - helper.log("rendering without audio"); - }).finally(() => { - if(config.debug) - console.time('encode video'); + bitrate -= 96; + }).catch(e => { + helper.error(e); + ffmpeg_args.unshift('-f', 'lavfi', '-r', fps, '-i', `color=c=black:s=${size.join("x")}`); + helper.log("rendering without audio"); + }).finally(() => { + if (config.debug) + console.time('encode video'); - ffmpeg_args.push( - '-filter_complex', `"overlay=(W-w)/2:shortest=1"`, - '-pix_fmt', 'yuv420p', '-r', fps, '-c:v', 'libx264', '-b:v', `${bitrate}k`, - '-c:a', 'aac', '-b:a', '164k', '-shortest', '-preset', 'veryfast', - '-movflags', 'faststart', '-g', fps, '-force_key_frames', '00:00:00.000', `${file_path}/video.mp4` - ); + ffmpeg_args.push( + '-filter_complex', `"overlay=(W-w)/2:shortest=1"`, + '-pix_fmt', 'yuv420p', '-r', fps, '-c:v', 'libx264', '-b:v', `${bitrate}k`, + '-c:a', 'aac', '-b:a', '96k', '-shortest', '-preset', 'veryfast', + '-movflags', 'faststart', '-g', fps, '-force_key_frames', '00:00:00.000', `${file_path}/video.mp4` + ); - const encodingProcessStart = Date.now(); + const encodingProcessStart = Date.now(); - let ffmpegProcess = spawn(ffmpeg, ffmpeg_args, { shell: true }); + let ffmpegProcess = spawn(ffmpeg, ffmpeg_args, {shell: true}); - ffmpegProcess.on('close', code => { - if(code > 0){ - resolveRender("Error encoding video") + ffmpegProcess.on('close', code => { + if (code > 0) { + resolveRender("Error encoding video") .then(() => { - fs.promises.rm(file_path, { recursive: true }).catch(helper.error); + fs.promises.rm(file_path, {recursive: true}).catch(helper.error); }).catch(console.error); - return false; - } + return false; + } - if(config.debug) - console.timeEnd('encode video'); + if (config.debug) + console.timeEnd('encode video'); - renderStatus[2] = `✓ encoding video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; + renderStatus[2] = `✓ encoding video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; - resolveRender({files: [{ + resolveRender({ + files: [{ attachment: `${file_path}/video.${options.type}`, name: `video.${options.type}` - }]}).then(() => { - fs.promises.rm(file_path, { recursive: true }).catch(helper.error); - }).catch(console.error); - }); - - ffmpegProcess.stderr.on('data', data => { - const line = data.toString(); - - if(!line.startsWith('frame=')) - return; + }] + }).then(() => { + fs.promises.rm(file_path, {recursive: true}).catch(helper.error); + }).catch(console.error); + }); - const frame = parseInt(line.substring(6).trim()); + ffmpegProcess.stderr.on('data', data => { + const line = data.toString(); - renderStatus[2] = `– encoding video (${Math.round(frame/amount_frames*100)}%)`; - }); + if (!line.startsWith('frame=')) + return; - pipeFrameLoop(ffmpegProcess, err => { - if(err){ - resolveRender("Error encoding video") - .then(() => { - fs.promises.rm(file_path, { recursive: true }).catch(helper.error); - }).catch(console.error); + helper.log(line); + const frame = parseInt(line.substring(6).trim()); - return false; - } - }); + renderStatus[2] = `– encoding video (${Math.round(frame / amount_frames * 100)}%)`; }); - } - const framesProcessStart = Date.now(); - - workers.forEach((worker, index) => { - worker.send({ - beatmap, - start_time: time + index * time_frame, - end_time: time + index * time_frame + modded_length, - time_frame: time_frame * threads, - file_path, - options, - threads, - current_frame: index, - size - }); - - worker.on('message', frame => { - frames_rendered.push(frame); - - renderStatus[1] = `– rendering frames (${Math.round(frames_rendered.length/amount_frames*100)}%)`; - }); + newPipeFrameLoop(ffmpegProcess, err => { + if (err) { + resolveRender("Error encoding video"); - worker.on('close', code => { - if(code > 0){ - cb("Error rendering beatmap"); return false; } + }).then(() => { helper.log("newPipeFrameLoop done") }) + + // pipeFrameLoop(ffmpegProcess, err => { + // if (err) { + // resolveRender("Error encoding video") + // .then(() => { + // fs.promises.rm(file_path, {recursive: true}).catch(helper.error); + // }).catch(console.error); + // + // return false; + // } + // }); + }); + } + + const framesProcessStart = Date.now(); + helper.log("Expected number of frames: " + amount_frames); + + + let workers = []; + for (let i = 0; i < threads; i++) { + workers.push( + fork(path.resolve(__dirname, 'render_worker.js')) + ); + } + + + let log_as_server = function(message) { + helper.log("[main thread] " + message); + } + + ipc.config.id = 'world'; + ipc.config.retry= 25; + // ipc.config.rawBuffer=true; + // ipc.config.encoding ='base64'; + ipc.config.logger = log_as_server + ipc.config.silent = true; + + let worker_idx = 0; + + ipc.serve( + function(){ + ipc.server.on( + 'connect', + function(socket){ + ipc.log("Client connected"); + } + ); + + ipc.server.on( + 'workerReady', + function(data,socket){ + log_as_server('Worker ready: ' + data); + ipc.server.emit(socket, 'setWorker', worker_idx); + + let worker_to_init = workers[worker_idx]; + worker_frame_queues[worker_idx] = new Queue(); + worker_frame_buffers[worker_idx] = []; // init frame buffer + + ipc.server.emit(socket, 'receiveWork', { + beatmap, + start_time: time + worker_idx * time_frame, + end_time: time + worker_idx * time_frame + modded_length, + time_frame: time_frame * threads, + file_path, + options, + threads, + current_frame: worker_idx, + size + }); + - done++; - if(done == threads){ - renderStatus[1] = `✓ rendering frames (${((Date.now() - framesProcessStart) / 1000).toFixed(3)}s)`; + worker_to_init.on('close', code => { + if (code > 0) { + cb("Error rendering beatmap"); + return false; + } - if(config.debug) - console.timeEnd('render beatmap'); - } - }); - }); - }); - }); - } + done++; + + if (done === threads) { + renderStatus[1] = `✓ rendering frames (${((Date.now() - framesProcessStart) / 1000).toFixed(3)}s)`; + + if (config.debug) + console.timeEnd('render beatmap'); + } + }); + + worker_idx++; + } + ); + + ipc.server.on( + 'app.framedata', + function(data, socket){ + // todo: implement slowing down of frame tap if video encoder can't keep up + let frame_wid = data.worker_id; + worker_frame_buffers[frame_wid].push(Buffer.from(data.frame_data, 'base64')); + if(data.last){ + ipc.server.emit(socket, 'app.framedataFullAck', 'ACK'); + log_as_server("received full frame from worker " + frame_wid + ", received in total: " + ++frame_counter); + + worker_frame_queues[frame_wid].enqueue(Buffer.concat(worker_frame_buffers[frame_wid])) + worker_frame_buffers[frame_wid].splice(0, worker_frame_buffers[frame_wid].length); + } + else { + ipc.server.emit(socket, 'app.framedataAck', 'ACK'); + } + }) + + ipc.server.on( + 'app.readyToTerminate', + function(data, socket){ + ipc.server.emit(socket, 'app.terminate', ''); + }) + } + ); + + ipc.server.start(); + + + + }); + } }; diff --git a/renderer/render_worker.js b/renderer/render_worker.js index 5e33d57..6dd7b32 100644 --- a/renderer/render_worker.js +++ b/renderer/render_worker.js @@ -1,8 +1,12 @@ + +// noinspection JSUnresolvedVariable + const { createCanvas, Image } = require('canvas'); const path = require('path'); const fs = require('fs').promises; const helper = require('../helper.js'); + const PLAYFIELD_WIDTH = 512; const PLAYFIELD_HEIGHT = 384; @@ -12,1191 +16,1307 @@ const KEY_OVERLAY_PADDING = 5; const FL_SIZES = [0.75, 0.6, 0.45]; // flashlight size relative to playfield height const resources = path.resolve(__dirname, "res"); +const ipc = require('node-ipc'); +const crypto = require("crypto"); + + + + + +let WAITING_FOR_SERVER_ACK = false; +let frame_counter = 0; + + + + + +let worker_id = crypto.randomBytes(3*4).toString('hex'); +let log_as_worker = function(message) { + helper.log("[worker " + worker_id + "] ", message); +} + +ipc.config.id = 'worker-' + worker_id; +ipc.config.retry= 200; +ipc.config.sync = true; +// ipc.config.rawBuffer=true; +// ipc.config.encoding ='base64'; +ipc.config.logger = log_as_worker +ipc.config.silent = true; + +ipc.connectTo( + 'world', + ipc.config.socketRoot + ipc.config.appspace + 'world', + function(){ + // ipc.of.world.on( + // 'connect', + // function(){ + // //make a 6 byte buffer for example + // const myBuffer=Buffer.alloc(6).fill(0); + // + // myBuffer.writeUInt16BE(0x02,0); + // myBuffer.writeUInt32BE(0xffeecc,2); + // + // ipc.log('## connected to world ##', ipc.config.delay); + // ipc.of.world.emit( + // myBuffer + // ); + // } + // ); + + ipc.of.world.on( + 'connect', + function(){ + ipc.log('## connected to world ##', ipc.config.delay); + ipc.of.world.emit('workerReady', worker_id); + } + ); + + ipc.of.world.on( + 'setWorker', + function(data){ + log_as_worker("Setting worker id: " + data); + worker_id = data; + // ipc.of.world.emit('app.framedata', {worker_id: worker_id, frame_data: 'sample_data_ignore'}); + } + ) + + ipc.of.world.on('app.framedataAck', function(data){ + // log_as_worker("Received ACK from server"); + // WAITING_FOR_SERVER_ACK = false; + }) + + ipc.of.world.on('app.framedataFullAck', function(data){ + // log_as_worker("Received ACK from server"); + WAITING_FOR_SERVER_ACK = false; + }) + + ipc.of.world.on('app.terminate', function(data){ + log_as_worker("Terminating..."); + process.exit(0); + }) + + ipc.of.world.on( + 'receiveWork', + async function (data){ + // log_as_worker(data); + if (1) { + let { beatmap, start_time, end_time, time_frame, file_path, options, threads, current_frame, size } = data; + + function resize(){ + active_playfield_width = canvas.width * 0.7; + active_playfield_height = active_playfield_width * (3/4); + let position = playfieldPosition(0, 0); + let size = playfieldPosition(PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT); + scale_multiplier = (size[0] - position[0]) / PLAYFIELD_WIDTH; + } + + // Convert osu pixels position to canvas coordinates + function playfieldPosition(x, y){ + let ratio_x = x / PLAYFIELD_WIDTH; + let ratio_y = y / PLAYFIELD_HEIGHT; + + return [ + active_playfield_width * ratio_x + canvas.width * 0.15, + active_playfield_height * ratio_y + canvas.height * 0.15 + ]; + } + + const flImages = []; + + let ctx; + let canvas; + function prepareCanvas(size){ + canvas = createCanvas(...size); + ctx = canvas.getContext("2d"); + resize(); + + if(options.flashlight){ + for(const sizeRelative of FL_SIZES){ + const flCanvas = createCanvas(size[0] * 2, size[0] * 2); + const flCtx = flCanvas.getContext("2d"); + + flCtx.fillStyle = 'black'; + flCtx.fillRect(0, 0, flCanvas.width, flCanvas.height); + + const flSize = sizeRelative * PLAYFIELD_HEIGHT * scale_multiplier / 2; + + const gradient = + flCtx.createRadialGradient( + flCanvas.width / 2, flCanvas.height / 2, + flSize * 0.9, + flCanvas.width / 2, flCanvas.height / 2, + flSize); + + gradient.addColorStop(0, 'rgba(0,0,0,1)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); + + flCtx.fillStyle = gradient; + flCtx.globalCompositeOperation = 'destination-out'; + + flCtx.beginPath(); + flCtx.arc(flCanvas.width / 2, flCanvas.height / 2, flSize, 0, 2 * Math.PI); + flCtx.fill(); + + flImages.push(flCanvas); + } + } + } + + function getCursorAtInterpolated(timestamp, replay){ + while(replay.lastCursor < replay.replay_data.length && replay.replay_data[replay.lastCursor].offset <= timestamp){ + replay.lastCursor++; + } + + let current = {...replay.replay_data[replay.lastCursor - 1]}; + let next = {...replay.replay_data[replay.lastCursor]} + + if(current === undefined || next === undefined){ + if(replay.replay_data.length > 0){ + return { + current: replay.replay_data[replay.replay_data.length - 1], + next: replay.replay_data[replay.replay_data.length - 1] + } + }else{ + return { + current: { + x: 0, + y: 0 + }, + next: { + x: 0, + y: 0 + } + } + } + } + + // Interpolate cursor position between two points for smooth motion + + let current_start = current.offset; + let next_start = next.offset; + + let pos_current = [current.x, current.y]; + let pos_next = [next.x, next.y]; + + timestamp -= current_start; + next_start -= current_start; + + let progress = timestamp / next_start; + + let distance = vectorDistance(pos_current, pos_next); + + let n = Math.max(1, progress * distance); + + if(distance > 0){ + current.x = pos_current[0] + (n / distance) * (pos_next[0] - pos_current[0]); + current.y = pos_current[1] + (n / distance) * (pos_next[1] - pos_current[1]); + } + + return {current: current, next: next}; + } + + function interpolateReplayData(replay){ + const interpolatedReplay = {lastCursor: 0, replay_data: []}; + + const frametime = 4; + + for(let timestamp = 0; timestamp < end_time; timestamp += frametime){ + const replayPoint = getCursorAtInterpolated(timestamp, replay).current; + replayPoint.offset = timestamp; + interpolatedReplay.replay_data.push(replayPoint); + } + + return interpolatedReplay; + } + + function getScoringFrames(timestamp, scoringFrames){ + const output = []; -let images = { - "arrow": path.resolve(resources, "images", "arrow.svg") -}; + let i = scoringFrames.findIndex(a => a.offset > timestamp - 2000); + + while(scoringFrames[i].offset <= timestamp){ + output.push(scoringFrames[i]); + i++; + } + + return output; + } + + function getCursorAt(timestamp, replay){ + while(replay.lastCursor < replay.replay_data.length && replay.replay_data[replay.lastCursor].offset <= timestamp){ + replay.lastCursor++; + } + + let current = replay.replay_data[replay.lastCursor - 1]; + let next = replay.replay_data[replay.lastCursor]; + let previous = []; + + for(let i = 0; i < Math.min(replay.lastCursor - 2, 20); i++) + previous.push(replay.replay_data[replay.lastCursor - i]); + + if(current === undefined || next === undefined){ + if(replay.replay_data.length > 0){ + return { + current: replay.replay_data[replay.replay_data.length - 1], + next: replay.replay_data[replay.replay_data.length - 1] + } + }else{ + return { + current: { + x: 0, + y: 0 + }, + next: { + x: 0, + y: 0 + } + } + } + } + + return {previous, current, next}; + } + + function vectorDistance(hitObject1, hitObject2){ + return Math.sqrt((hitObject2[0] - hitObject1[0]) * (hitObject2[0] - hitObject1[0]) + + (hitObject2[1] - hitObject1[1]) * (hitObject2[1] - hitObject1[1])); + } + + function processFrame(time, options){ + let hitObjectsOnScreen = []; + + ctx.globalAlpha = 1; + + // Generate array with all hit objects currently visible + beatmap.hitObjects.forEach(hitObject => { + if(time >= hitObject.startTime - beatmap.TimePreempt && hitObject.endTime - time > -200) + hitObjectsOnScreen.push(hitObject); + }); + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if(options.black){ + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + hitObjectsOnScreen.sort(function(a, b){ return a.startTime - b.startTime; }); + + ctx.strokeStyle = 'rgba(255,255,255,0.3)'; + ctx.lineWidth = 3 * scale_multiplier; + ctx.shadowColor = 'transparent'; + + // Draw follow points + hitObjectsOnScreen.forEach(function(hitObject, index){ + if(index < hitObjectsOnScreen.length - 1){ + let nextObject = hitObjectsOnScreen[index + 1]; + + if(isNaN(hitObject.endPosition) && isNaN(nextObject.position)) + return false; -process.on('uncaughtException', err => { - helper.error(err); - process.exit(1); -}); - -process.on('message', async obj => { - let { beatmap, start_time, end_time, time_frame, file_path, options, threads, current_frame, size, ctx } = obj; - - function resize(){ - active_playfield_width = canvas.width * 0.7; - active_playfield_height = active_playfield_width * (3/4); - let position = playfieldPosition(0, 0); - let size = playfieldPosition(PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT); - scale_multiplier = (size[0] - position[0]) / PLAYFIELD_WIDTH; - } - - // Convert osu pixels position to canvas coordinates - function playfieldPosition(x, y){ - let ratio_x = x / PLAYFIELD_WIDTH; - let ratio_y = y / PLAYFIELD_HEIGHT; - - return [ - active_playfield_width * ratio_x + canvas.width * 0.15, - active_playfield_height * ratio_y + canvas.height * 0.15 - ]; - } - - const flImages = []; - - function prepareCanvas(size){ - canvas = createCanvas(...size); - ctx = canvas.getContext("2d"); - resize(); - - if(options.flashlight){ - for(const sizeRelative of FL_SIZES){ - const flCanvas = createCanvas(size[0] * 2, size[0] * 2); - const flCtx = flCanvas.getContext("2d"); - - flCtx.fillStyle = 'black'; - flCtx.fillRect(0, 0, flCanvas.width, flCanvas.height); - - const flSize = sizeRelative * PLAYFIELD_HEIGHT * scale_multiplier / 2; - - const gradient = - flCtx.createRadialGradient( - flCanvas.width / 2, flCanvas.height / 2, - flSize * 0.9, - flCanvas.width / 2, flCanvas.height / 2, - flSize); - - gradient.addColorStop(0, 'rgba(0,0,0,1)'); - gradient.addColorStop(1, 'rgba(0,0,0,0)'); - - flCtx.fillStyle = gradient; - flCtx.globalCompositeOperation = 'destination-out'; - - flCtx.beginPath(); - flCtx.arc(flCanvas.width / 2, flCanvas.height / 2, flSize, 0, 2 * Math.PI); - flCtx.fill(); - - flImages.push(flCanvas); - } - } - } - - function getCursorAtInterpolated(timestamp, replay){ - while(replay.lastCursor < replay.replay_data.length && replay.replay_data[replay.lastCursor].offset <= timestamp){ - replay.lastCursor++; - } - - let current = {...replay.replay_data[replay.lastCursor - 1]}; - let next = {...replay.replay_data[replay.lastCursor]} - - if(current === undefined || next === undefined){ - if(replay.replay_data.length > 0){ - return { - current: replay.replay_data[replay.replay_data.length - 1], - next: replay.replay_data[replay.replay_data.length - 1] - } - }else{ - return { - current: { - x: 0, - y: 0 - }, - next: { - x: 0, - y: 0 - } - } - } - } - - // Interpolate cursor position between two points for smooth motion - - let current_start = current.offset; - let next_start = next.offset; - - let pos_current = [current.x, current.y]; - let pos_next = [next.x, next.y]; - - timestamp -= current_start; - next_start -= current_start; - - let progress = timestamp / next_start; - - let distance = vectorDistance(pos_current, pos_next); - - let n = Math.max(1, progress * distance); - - if(distance > 0){ - current.x = pos_current[0] + (n / distance) * (pos_next[0] - pos_current[0]); - current.y = pos_current[1] + (n / distance) * (pos_next[1] - pos_current[1]); - } - - return {current: current, next: next}; - } - - function interpolateReplayData(replay){ - const interpolatedReplay = {lastCursor: 0, replay_data: []}; - - const frametime = 4; - - for(let timestamp = 0; timestamp < end_time; timestamp += frametime){ - const replayPoint = getCursorAtInterpolated(timestamp, replay).current; - replayPoint.offset = timestamp; - interpolatedReplay.replay_data.push(replayPoint); - } - - return interpolatedReplay; - } - - function getScoringFrames(timestamp, scoringFrames){ - const output = []; + let distance = vectorDistance(hitObject.endPosition, nextObject.position); - let i = scoringFrames.findIndex(a => a.offset > timestamp - 2000); + if(time >= (nextObject.startTime - beatmap.TimePreempt) && time < (nextObject.startTime + beatmap.HitWindow50) && distance > 80){ + let start_position = playfieldPosition(...hitObject.endPosition); + let end_position = playfieldPosition(...nextObject.position); - while(scoringFrames[i].offset <= timestamp){ - output.push(scoringFrames[i]); - i++; - } + let progress_0 = nextObject.startTime - beatmap.TimePreempt - return output; - } + let a = progress_0; - function getCursorAt(timestamp, replay){ - while(replay.lastCursor < replay.replay_data.length && replay.replay_data[replay.lastCursor].offset <= timestamp){ - replay.lastCursor++; - } + progress_0 += time - progress_0; + let progress_1 = nextObject.startTime - beatmap.TimeFadein; - let current = replay.replay_data[replay.lastCursor - 1]; - let next = replay.replay_data[replay.lastCursor]; - let previous = []; + progress_1 -= a + progress_0 -= a; - for(let i = 0; i < Math.min(replay.lastCursor - 2, 20); i++) - previous.push(replay.replay_data[replay.lastCursor - i]); + let progress = Math.min(1, progress_0 / progress_1 * 2); - if(current === undefined || next === undefined){ - if(replay.replay_data.length > 0){ - return { - current: replay.replay_data[replay.replay_data.length - 1], - next: replay.replay_data[replay.replay_data.length - 1] - } - }else{ - return { - current: { - x: 0, - y: 0 - }, - next: { - x: 0, - y: 0 - } - } - } - } + let v = vectorSubtract(end_position, start_position); - return {previous, current, next}; - } + v[0] *= progress; + v[1] *= progress; - function vectorDistance(hitObject1, hitObject2){ - return Math.sqrt((hitObject2[0] - hitObject1[0]) * (hitObject2[0] - hitObject1[0]) - + (hitObject2[1] - hitObject1[1]) * (hitObject2[1] - hitObject1[1])); - } - function processFrame(time, options){ - let hitObjectsOnScreen = []; + ctx.beginPath(); + ctx.moveTo(...start_position); + ctx.lineTo(vectorAdd(start_position, v[0])); + ctx.stroke(); - ctx.globalAlpha = 1; + //then shift x by cos(angle)*radius and y by sin(angle)*radius (TODO) + } + } + }); - // Generate array with all hit objects currently visible - beatmap.hitObjects.forEach(hitObject => { - if(time >= hitObject.startTime - beatmap.TimePreempt && hitObject.endTime - time > -200) - hitObjectsOnScreen.push(hitObject); - }); + // Sort hit objects from latest to earliest so the earliest hit object is at the front + hitObjectsOnScreen.reverse(); - ctx.clearRect(0, 0, canvas.width, canvas.height); + hitObjectsOnScreen.forEach(function(hitObject, index){ + // Check if hit object could be visible at current timestamp + if(time < hitObject.startTime || hitObject.objectName !== "circle" && time < hitObject.endTime + 200){ + // Apply approach rate + let opacity = (time - (hitObject.startTime - beatmap.TimePreempt)) / (beatmap.TimePreempt - beatmap.TimeFadein); - if(options.black){ - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } + if(hitObject.objectName !== 'circle') + opacity = 1 - (time - hitObject.endTime) / 200; - hitObjectsOnScreen.sort(function(a, b){ return a.startTime - b.startTime; }); + // Calculate relative approach circle size (number from 0 to 1) + let approachCircle = 1 - (time - (hitObject.startTime - beatmap.TimeFadein)) / beatmap.TimeFadein; - ctx.strokeStyle = 'rgba(255,255,255,0.3)'; - ctx.lineWidth = 3 * scale_multiplier; - ctx.shadowColor = 'transparent'; + if(approachCircle < 0) approachCircle = 0; + if(opacity > 1) opacity = 1; - // Draw follow points - hitObjectsOnScreen.forEach(function(hitObject, index){ - if(index < hitObjectsOnScreen.length - 1){ - let nextObject = hitObjectsOnScreen[index + 1]; + ctx.shadowBlur = 4 * scale_multiplier; + ctx.fillStyle = "rgba(40,40,40,0.2)"; - if(isNaN(hitObject.endPosition) && isNaN(nextObject.position)) - return false; + let followpoint_index; + let followpoint_progress = 0; - let distance = vectorDistance(hitObject.endPosition, nextObject.position); + // Draw slider + if(hitObject.objectName === "slider"){ + let sliderOpacity = opacity; - if(time >= (nextObject.startTime - beatmap.TimePreempt) && time < (nextObject.startTime + beatmap.HitWindow50) && distance > 80){ - let start_position = playfieldPosition(...hitObject.endPosition); - let end_position = playfieldPosition(...nextObject.position); + if(options.hidden){ + const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; - let progress_0 = nextObject.startTime - beatmap.TimePreempt + if(time >= fadeOutStartTime) + sliderOpacity = 1 - (time - fadeOutStartTime) / (hitObject.endTime - fadeOutStartTime); - let a = progress_0; + if(sliderOpacity < 0) + sliderOpacity = 0; + } - progress_0 += time - progress_0; - let progress_1 = nextObject.startTime - beatmap.TimeFadein; + ctx.globalAlpha = sliderOpacity; - progress_1 -= a - progress_0 -= a; + ctx.lineWidth = 5 * scale_multiplier; + ctx.strokeStyle = "rgba(255,255,255,0.7)"; - let progress = Math.min(1, progress_0 / progress_1 * 2); + let snakingStart = hitObject.startTime - beatmap.TimePreempt; + let snakingFinish = hitObject.startTime - beatmap.TimeFadein; - let v = vectorSubtract(end_position, start_position); + let snakingProgress = Math.max(0, Math.min(1, (time - snakingStart) / (snakingFinish - snakingStart))); - v[0] *= progress; - v[1] *= progress; + let render_dots = []; + for(let x = 0; x < Math.floor(hitObject.SliderDots.length * snakingProgress); x++) + render_dots.push(hitObject.SliderDots[x]); - ctx.beginPath(); - ctx.moveTo(...start_position); - ctx.lineTo(vectorAdd(start_position, v[0])); - ctx.stroke(); + // Use stroke with rounded ends to "fake" a slider path + ctx.beginPath(); + ctx.lineCap = "round"; + ctx.strokeStyle = 'rgba(255,255,255,0.7)'; + ctx.shadowColor = 'transparent'; + ctx.lineJoin = "round" - //then shift x by cos(angle)*radius and y by sin(angle)*radius (TODO) - } - } - }); + ctx.lineWidth = scale_multiplier * beatmap.Radius * 2 - // Sort hit objects from latest to earliest so the earliest hit object is at the front - hitObjectsOnScreen.reverse(); + // Draw a path through all slider dots generated earlier + for(let x = 0; x < render_dots.length; x++){ + let dot = render_dots[x]; + let position = playfieldPosition(...dot); - hitObjectsOnScreen.forEach(function(hitObject, index){ - // Check if hit object could be visible at current timestamp - if(time < hitObject.startTime || hitObject.objectName != "circle" && time < hitObject.endTime + 200){ - // Apply approach rate - let opacity = (time - (hitObject.startTime - beatmap.TimePreempt)) / (beatmap.TimePreempt - beatmap.TimeFadein); + if(x === 0){ + ctx.moveTo(...position); + }else{ + ctx.lineTo(...position); + } + } - if(hitObject.objectName != 'circle') - opacity = 1 - (time - hitObject.endTime) / 200; + ctx.stroke(); - // Calculate relative approach circle size (number from 0 to 1) - let approachCircle = 1 - (time - (hitObject.startTime - beatmap.TimeFadein)) / beatmap.TimeFadein; + ctx.lineWidth = scale_multiplier * (beatmap.Radius * 2 - 10); + ctx.strokeStyle = 'rgba(0,0,0,0.6)'; - if(approachCircle < 0) approachCircle = 0; - if(opacity > 1) opacity = 1; + ctx.stroke(); - ctx.shadowBlur = 4 * scale_multiplier; - ctx.fillStyle = "rgba(40,40,40,0.2)"; + let currentTurn = 0, currentOffset, currentTurnStart; - let followpoint_index; - let followpoint_progress = 0; + // Get slider dot corresponding to the current follow point position + if(time >= hitObject.startTime && time <= hitObject.endTime){ + currentTurn = Math.floor((time - hitObject.startTime) / (hitObject.duration / hitObject.repeatCount)); + currentTurnStart = hitObject.startTime + hitObject.duration / hitObject.repeatCount * currentTurn; + currentOffset = (time - hitObject.startTime) / (hitObject.duration / hitObject.repeatCount) - currentTurn; - // Draw slider - if(hitObject.objectName == "slider"){ - let sliderOpacity = opacity; + let dot_index = 0; - if(options.hidden){ - const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; - - if(time >= fadeOutStartTime) - sliderOpacity = 1 - (time - fadeOutStartTime) / (hitObject.endTime - fadeOutStartTime); - - if(sliderOpacity < 0) - sliderOpacity = 0; - } + if(currentTurn % 2 === 0) + dot_index = currentOffset * hitObject.SliderDots.length; + else + dot_index = (1 - currentOffset) * hitObject.SliderDots.length; - ctx.globalAlpha = sliderOpacity; + followpoint_index = Math.floor(dot_index); - ctx.lineWidth = 5 * scale_multiplier; - ctx.strokeStyle = "rgba(255,255,255,0.7)"; + /* Progress number from 0 to 1 to check how much relative distance to the next slider dot is left, + used in interpolation later to always have smooth follow points */ + followpoint_progress = dot_index - followpoint_index; + }else{ + if(time < hitObject.startTime){ + currentOffset = 0; + currentTurnStart = hitObject.startTime - beatmap.TimePreempt; + }else{ + currentOffset = 1; + currentTurnStart = hitObject.endTime; + } + } - let snakingStart = hitObject.startTime - beatmap.TimePreempt; - let snakingFinish = hitObject.startTime - beatmap.TimeFadein; + // Render slider ticks (WIP) + if(time <= hitObject.endTime){ + ctx.strokeStyle = 'rgba(255,255,255,0.8)'; + ctx.lineWidth = 5 * scale_multiplier; - let snakingProgress = Math.max(0, Math.min(1, (time - snakingStart) / (snakingFinish - snakingStart))); + let slider_ticks = hitObject.SliderTicks.slice(); - let render_dots = []; + // Reverse slider ticks depending on current slider direction + if(currentTurn > 0 && currentTurn % 2 !== 0) + slider_ticks.reverse(); - for(let x = 0; x < Math.floor(hitObject.SliderDots.length * snakingProgress); x++) - render_dots.push(hitObject.SliderDots[x]); + let max = Math.floor(slider_ticks.length * snakingProgress); - // Use stroke with rounded ends to "fake" a slider path - ctx.beginPath(); - ctx.lineCap = "round"; - ctx.strokeStyle = 'rgba(255,255,255,0.7)'; - ctx.shadowColor = 'transparent'; - ctx.lineJoin = "round" + let offset = time - currentTurnStart; - ctx.lineWidth = scale_multiplier * beatmap.Radius * 2 + for(let x = 0; x < max; x++){ + if(currentTurn > 0) + ctx.globalAlpha = Math.min(1, Math.max(0, Math.min(1, (time - x * 40 - currentTurnStart) / 50))); + else if(time < hitObject.startTime) + ctx.globalAlpha = Math.min(1, Math.max(0, Math.min(1, (time - (max - x) * 40 - currentTurnStart) / 50))); - // Draw a path through all slider dots generated earlier - for(let x = 0; x < render_dots.length; x++){ - let dot = render_dots[x]; - let position = playfieldPosition(...dot); + let tick = slider_ticks[x]; - if(x == 0){ - ctx.moveTo(...position); - }else{ - ctx.lineTo(...position); - } - } + if(currentTurn > 0 && currentTurn % 2 !== 0) + tick.offset = tick.reverseOffset; - ctx.stroke(); + if(followpoint_index && tick.offset < offset) + continue; - ctx.lineWidth = scale_multiplier * (beatmap.Radius * 2 - 10); - ctx.strokeStyle = 'rgba(0,0,0,0.6)'; + let position = playfieldPosition(...tick.position); + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * beatmap.Radius / 5, 0, 2 * Math.PI, false); + ctx.stroke(); + } - ctx.stroke(); + ctx.globalAlpha = opacity; + } - let currentTurn = 0, currentOffset, currentTurnStart; + // Render repeat arrow + for(let x = 1; x < hitObject.repeatCount; x++){ + let repeatOffset = hitObject.startTime + x * (hitObject.duration / hitObject.repeatCount); + let fadeInStart = x === 1 ? snakingFinish : repeatOffset - (hitObject.duration / hitObject.repeatCount) * 2; + let repeatPosition = (x - 1) % 2 === 0 ? hitObject.endPosition : hitObject.position; - // Get slider dot corresponding to the current follow point position - if(time >= hitObject.startTime && time <= hitObject.endTime){ - currentTurn = Math.floor((time - hitObject.startTime) / (hitObject.duration / hitObject.repeatCount)); - currentTurnStart = hitObject.startTime + hitObject.duration / hitObject.repeatCount * currentTurn; - currentOffset = (time - hitObject.startTime) / (hitObject.duration / hitObject.repeatCount) - currentTurn; + let timeSince = Math.max(0, Math.min(1, (time - repeatOffset) / 200)); - let dot_index = 0; + if(time >= repeatOffset) + ctx.globalAlpha = (1 - timeSince); + else + ctx.globalAlpha = Math.min(1, Math.max(0, (time - fadeInStart) / 50)); - if(currentTurn % 2 == 0) - dot_index = currentOffset * hitObject.SliderDots.length; - else - dot_index = (1 - currentOffset) * hitObject.SliderDots.length; + let sizeFactor = 1 + timeSince * 0.3; - followpoint_index = Math.floor(dot_index); + let comparePosition = + (x - 1) % 2 === 0 ? hitObject.SliderDots[hitObject.SliderDots.length - 2] : hitObject.SliderDots[1]; - /* Progress number from 0 to 1 to check how much relative distance to the next slider dot is left, - used in interpolation later to always have smooth follow points */ - followpoint_progress = dot_index - followpoint_index; - }else{ - if(time < hitObject.startTime){ - currentOffset = 0; - currentTurnStart = hitObject.startTime - beatmap.TimePreempt; - }else{ - currentOffset = 1; - currentTurnStart = hitObject.endTime; - } - } + let repeatDirection = Math.atan2(comparePosition[1] - repeatPosition[1], comparePosition[0] - repeatPosition[0]); - // Render slider ticks (WIP) - if(time <= hitObject.endTime){ - ctx.strokeStyle = 'rgba(255,255,255,0.8)'; - ctx.lineWidth = 5 * scale_multiplier; + let position = playfieldPosition(...repeatPosition); - let slider_ticks = hitObject.SliderTicks.slice(); + let size = beatmap.Radius * 2 * scale_multiplier; - // Reverse slider ticks depending on current slider direction - if(currentTurn > 0 && currentTurn % 2 != 0) - slider_ticks.reverse(); + ctx.save(); - let max = Math.floor(slider_ticks.length * snakingProgress); + ctx.translate(...position); + ctx.rotate(repeatDirection); - let offset = time - currentTurnStart; + position = [0, 0]; - for(let x = 0; x < max; x++){ - if(currentTurn > 0) - ctx.globalAlpha = Math.min(1, Math.max(0, Math.min(1, (time - x * 40 - currentTurnStart) / 50))); - else if(time < hitObject.startTime) - ctx.globalAlpha = Math.min(1, Math.max(0, Math.min(1, (time - (max - x) * 40 - currentTurnStart) / 50))); + ctx.lineWidth = 5 * scale_multiplier; + ctx.beginPath(); + ctx.strokeStyle = "rgba(255,255,255,0.85)"; - let tick = slider_ticks[x]; + if(!options.noshadow) + ctx.shadowColor = "rgba(0,0,0,0.7)"; - if(currentTurn > 0 && currentTurn % 2 != 0) - tick.offset = tick.reverseOffset; + // Fill circle with combo color instead of leaving see-through circles + if(options.fill){ + ctx.beginPath(); + ctx.fillStyle = hitObject.Color; + ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); + ctx.fill(); + } - if(followpoint_index && tick.offset < offset) - continue; + // Draw circle border + ctx.beginPath(); + ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.stroke(); - let position = playfieldPosition(...tick.position); - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * beatmap.Radius / 5, 0, 2 * Math.PI, false); - ctx.stroke(); - } + ctx.fillStyle = 'white'; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; - ctx.globalAlpha = opacity; - } + let fontSize = 18; + fontSize += 16 * (1 - (beatmap.CircleSize / 10)); - // Render repeat arrow - for(let x = 1; x < hitObject.repeatCount; x++){ - let repeatOffset = hitObject.startTime + x * (hitObject.duration / hitObject.repeatCount); - let fadeInStart = x == 1 ? snakingFinish : repeatOffset - (hitObject.duration / hitObject.repeatCount) * 2; - let repeatPosition = (x - 1) % 2 == 0 ? hitObject.endPosition : hitObject.position; + fontSize *= scale_multiplier * sizeFactor; - let timeSince = Math.max(0, Math.min(1, (time - repeatOffset) / 200)); + // Draw combo number on circle + ctx.font = `${fontSize}px sans-serif`; - if(time >= repeatOffset) - ctx.globalAlpha = (1 - timeSince); - else - ctx.globalAlpha = Math.min(1, Math.max(0, (time - fadeInStart) / 50)); + ctx.fillText("➤", ...position); - let sizeFactor = 1 + timeSince * 0.3; + /* this doesn't render correctly for some reason??? + using text for now I guess (TODO: FIX) */ + //ctx.drawImage(images.arrow, ...position, size, size); - let comparePosition = - (x - 1) % 2 == 0 ? hitObject.SliderDots[hitObject.SliderDots.length - 2] : hitObject.SliderDots[1]; + ctx.restore(); + } - let repeatDirection = Math.atan2(comparePosition[1] - repeatPosition[1], comparePosition[0] - repeatPosition[0]); + ctx.globalAlpha = opacity; + } - let position = playfieldPosition(...repeatPosition); + let circleOpacity = opacity; - let size = beatmap.Radius * 2 * scale_multiplier; + if(options.hidden && circleOpacity >= 1){ + const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; - ctx.save(); + if(time >= fadeOutStartTime) + circleOpacity = 1 - (time - fadeOutStartTime) / (beatmap.TimePreempt * 0.3); - ctx.translate(...position); - ctx.rotate(repeatDirection); + if(circleOpacity < 0) + circleOpacity = 0; + } - position = [0, 0]; + ctx.globalAlpha = circleOpacity; - ctx.lineWidth = 5 * scale_multiplier; - ctx.beginPath(); - ctx.strokeStyle = "rgba(255,255,255,0.85)"; + // Draw circles or slider heads + if(hitObject.objectName !== "spinner"){ + ctx.lineWidth = 5 * scale_multiplier; + ctx.beginPath(); + ctx.strokeStyle = "rgba(255,255,255,0.85)"; - if(!options.noshadow) - ctx.shadowColor = "rgba(0,0,0,0.7)"; + if(time < hitObject.startTime){ + if(!options.noshadow) + ctx.shadowColor = "rgba(0,0,0,0.7)"; - // Fill circle with combo color instead of leaving see-through circles - if(options.fill){ - ctx.beginPath(); - ctx.fillStyle = hitObject.Color; - ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); - ctx.fill(); - } + let position = playfieldPosition(...hitObject.position); - // Draw circle border - ctx.beginPath(); - ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.stroke(); + // Fill circle with combo color instead of leaving see-through circles + if(options.fill){ + ctx.beginPath(); + ctx.fillStyle = hitObject.Color; + ctx.arc(...position, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); + ctx.fill(); + } - ctx.fillStyle = 'white'; - ctx.textBaseline = "middle"; - ctx.textAlign = "center"; + // Draw circle border + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.stroke(); - let fontSize = 18; - fontSize += 16 * (1 - (beatmap.CircleSize / 10)); + ctx.fillStyle = 'white'; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; - fontSize *= scale_multiplier * sizeFactor; + let fontSize = 16; + fontSize += 16 * (1 - (beatmap.CircleSize / 10)); - // Draw combo number on circle - ctx.font = `${fontSize}px sans-serif`; + fontSize *= scale_multiplier; - ctx.fillText("➤", ...position); + // Draw combo number on circle + ctx.font = `${fontSize}px sans-serif`; + ctx.fillText(hitObject.ComboNumber, position[0], position[1]); - /* this doesn't render correctly for some reason??? - using text for now I guess (TODO: FIX) */ - //ctx.drawImage(images.arrow, ...position, size, size); + // Draw approach circle + if(approachCircle > 0 && !options.hidden){ + ctx.strokeStyle = 'white'; + ctx.lineWidth = 2 * scale_multiplier; + ctx.beginPath(); + let position = playfieldPosition(...hitObject.position); + ctx.arc(...position, scale_multiplier * (beatmap.Radius + approachCircle * (beatmap.Radius * 2)), 0, 2 * Math.PI, false); + ctx.stroke(); + } + } - ctx.restore(); - } + // Draw follow point if there's currently one visible + if(followpoint_index + && Array.isArray(hitObject.SliderDots[followpoint_index]) + && hitObject.SliderDots[followpoint_index].length === 2 + ){ + let pos_current = hitObject.SliderDots[followpoint_index]; - ctx.globalAlpha = opacity; - } + if(hitObject.SliderDots.length - 1 > followpoint_index){ + // Interpolate follow point position - let circleOpacity = opacity; + let pos_next = hitObject.SliderDots[followpoint_index + 1]; - if(options.hidden && circleOpacity >= 1){ - const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; + let distance = vectorDistance(pos_current, pos_next); - if(time >= fadeOutStartTime) - circleOpacity = 1 - (time - fadeOutStartTime) / (beatmap.TimePreempt * 0.3); + let n = Math.max(1, followpoint_progress * distance); - if(circleOpacity < 0) - circleOpacity = 0; - } + if(distance > 0){ + pos_current = [ + pos_current[0] + (n / distance) * (pos_next[0] - pos_current[0]), + pos_current[1] + (n / distance) * (pos_next[1] - pos_current[1]) + ] + } + } - ctx.globalAlpha = circleOpacity; + ctx.globalAlpha = 1; - // Draw circles or slider heads - if(hitObject.objectName != "spinner"){ - ctx.lineWidth = 5 * scale_multiplier; - ctx.beginPath(); - ctx.strokeStyle = "rgba(255,255,255,0.85)"; + let position; - if(time < hitObject.startTime){ - if(!options.noshadow) - ctx.shadowColor = "rgba(0,0,0,0.7)"; + // Draw follow point in circle - let position = playfieldPosition(...hitObject.position); + ctx.fillStyle = "rgba(255,255,255,0.3)"; + ctx.beginPath(); - // Fill circle with combo color instead of leaving see-through circles - if(options.fill){ - ctx.beginPath(); - ctx.fillStyle = hitObject.Color; - ctx.arc(...position, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); - ctx.fill(); - } + position = playfieldPosition(...pos_current); + ctx.arc(...position, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); + ctx.fill(); - // Draw circle border - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.stroke(); + // Draw follow circle visible around the follow point - ctx.fillStyle = 'white'; - ctx.textBaseline = "middle"; - ctx.textAlign = "center"; + ctx.fillStyle = "rgba(255,255,255,0.8)"; + ctx.beginPath(); + + position = playfieldPosition(...pos_current); + ctx.arc(...position, scale_multiplier * (beatmap.FollowpointRadius), 0, 2 * Math.PI, false); + ctx.stroke(); + } + + }else{ + // Draw spinner + ctx.strokeStyle = "white"; + ctx.globalAlpha = opacity; + ctx.lineWidth = 10 * scale_multiplier; + + let position = playfieldPosition(PLAYFIELD_WIDTH / 2, PLAYFIELD_HEIGHT / 2); + + // Rotate spinner (WIP) + /* + if(beatmap.Replay && time >= hitObject.startTime){ + let replay_point = getCursorAt(time, beatmap.Replay); + + if(replay_point){ + let { current } = replay_point; + + let radians = Math.atan2(current.y - PLAYFIELD_WIDTH / 2, current.x - PLAYFIELD_HEIGHT / 2); + + position = [ + position[0] + 2.5 * Math.cos(radians), + position[1] + 2.5 * Math.sin(radians) + ]; + } + } + */ - let fontSize = 16; - fontSize += 16 * (1 - (beatmap.CircleSize / 10)); + // Outer spinner circle + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * 240, 0, 2 * Math.PI, false); + ctx.stroke(); - fontSize *= scale_multiplier; + // Inner spinner circle + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * 30, 0, 2 * Math.PI, false); + ctx.stroke(); + } + } - // Draw combo number on circle - ctx.font = `${fontSize}px sans-serif`; - ctx.fillText(hitObject.ComboNumber, position[0], position[1]); + if(!options.hidden && time >= hitObject.startTime && hitObject.startTime - time > -200){ + // Draw fading out circles + if(hitObject.objectName !== "spinner"){ + // Increase circle size the further it's faded out + let hitOffset = 0; - // Draw approach circle - if(approachCircle > 0 && !options.hidden){ - ctx.strokeStyle = 'white'; - ctx.lineWidth = 2 * scale_multiplier; - ctx.beginPath(); - let position = playfieldPosition(...hitObject.position); - ctx.arc(...position, scale_multiplier * (beatmap.Radius + approachCircle * (beatmap.Radius * 2)), 0, 2 * Math.PI, false); - ctx.stroke(); - } - } + if(beatmap.Replay.auto !== true){ + if(hitObject.hitOffset == null) + hitOffset += beatmap.HitWindow50; + else + hitOffset += hitObject.hitOffset; + } - // Draw follow point if there's currently one visible - if(followpoint_index - && Array.isArray(hitObject.SliderDots[followpoint_index]) - && hitObject.SliderDots[followpoint_index].length == 2 - ){ - let pos_current = hitObject.SliderDots[followpoint_index]; + let timeSince = Math.min(1, Math.max(0, (time - (hitObject.startTime + hitOffset)) / 200)); + let opacity = 1 - timeSince; + let sizeFactor = 1 + timeSince * 0.3; - if(hitObject.SliderDots.length - 1 > followpoint_index){ - // Interpolate follow point position + ctx.globalAlpha = opacity; - let pos_next = hitObject.SliderDots[followpoint_index + 1]; + if(!options.noshadow) + ctx.shadowColor = "rgba(0,0,0,0.7)"; - let distance = vectorDistance(pos_current, pos_next); + ctx.lineWidth = 6 * scale_multiplier; + ctx.beginPath(); + ctx.strokeStyle = "rgba(255,255,255,0.85)"; - let n = Math.max(1, followpoint_progress * distance); + let position = playfieldPosition(...hitObject.position); - if(distance > 0){ - pos_current = [ - pos_current[0] + (n / distance) * (pos_next[0] - pos_current[0]), - pos_current[1] + (n / distance) * (pos_next[1] - pos_current[1]) - ] - } - } + if(options.fill){ + ctx.beginPath(); + ctx.fillStyle = hitObject.Color; + ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); + ctx.fill(); + } - ctx.globalAlpha = 1; + ctx.beginPath(); + ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.stroke(); - let position; + ctx.fillStyle = 'white'; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; - // Draw follow point in circle + let fontSize = 16; + fontSize += 16 * (1 - (beatmap.CircleSize / 10)); - ctx.fillStyle = "rgba(255,255,255,0.3)"; - ctx.beginPath(); + fontSize *= scale_multiplier * sizeFactor; - position = playfieldPosition(...pos_current); - ctx.arc(...position, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); - ctx.fill(); + ctx.font = `${fontSize}px sans-serif`; + ctx.fillText(hitObject.ComboNumber, ...position); + } + } + }); - // Draw follow circle visible around the follow point + if(options.analyze){ + for(const hitObject of beatmap.hitObjects){ + if(hitObject.objectName === 'spinner') + continue; - ctx.fillStyle = "rgba(255,255,255,0.8)"; - ctx.beginPath(); - - position = playfieldPosition(...pos_current); - ctx.arc(...position, scale_multiplier * (beatmap.FollowpointRadius), 0, 2 * Math.PI, false); - ctx.stroke(); - } - - }else{ - // Draw spinner - ctx.strokeStyle = "white"; - ctx.globalAlpha = opacity; - ctx.lineWidth = 10 * scale_multiplier; - - let position = playfieldPosition(PLAYFIELD_WIDTH / 2, PLAYFIELD_HEIGHT / 2); - - // Rotate spinner (WIP) - /* - if(beatmap.Replay && time >= hitObject.startTime){ - let replay_point = getCursorAt(time, beatmap.Replay); - - if(replay_point){ - let { current } = replay_point; - - let radians = Math.atan2(current.y - PLAYFIELD_WIDTH / 2, current.x - PLAYFIELD_HEIGHT / 2); - - position = [ - position[0] + 2.5 * Math.cos(radians), - position[1] + 2.5 * Math.sin(radians) - ]; - } - } - */ + if(hitObject.hitResult > 0 && hitObject.objectName === 'circle' + || hitObject.MissedSliderStart < 1 && hitObject.objectName === 'slider') + continue; - // Outer spinner circle - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * 240, 0, 2 * Math.PI, false); - ctx.stroke(); + if(time < hitObject.startTime) + continue; - // Inner spinner circle - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * 30, 0, 2 * Math.PI, false); - ctx.stroke(); - } - } + if(time - hitObject.startTime > 750) + continue; - if(!options.hidden && time >= hitObject.startTime && hitObject.startTime - time > -200){ - // Draw fading out circles - if(hitObject.objectName != "spinner"){ - // Increase circle size the further it's faded out - let hitOffset = 0; + const position = playfieldPosition(...hitObject.position); - if(beatmap.Replay.auto !== true){ - if(hitObject.hitOffset == null) - hitOffset += beatmap.HitWindow50; - else - hitOffset += hitObject.hitOffset; - } + ctx.globalAlpha = 1; + ctx.lineWidth = 3 * scale_multiplier; + ctx.strokeStyle = options.fill ? '#fa2f2f' : 'white'; + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.stroke(); + } + } - let timeSince = Math.min(1, Math.max(0, (time - (hitObject.startTime + hitOffset)) / 200)); - let opacity = 1 - timeSince; - let sizeFactor = 1 + timeSince * 0.3; + if(beatmap.ScoringFrames && beatmap.Replay.auto !== true){ + //const scoringFrames = getScoringFrames(time, beatmap.ScoringFrames); - ctx.globalAlpha = opacity; + let previousFramesIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time - 5000); - if(!options.noshadow) - ctx.shadowColor = "rgba(0,0,0,0.7)"; + let currentFrameIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time) - 1; - ctx.lineWidth = 6 * scale_multiplier; - ctx.beginPath(); - ctx.strokeStyle = "rgba(255,255,255,0.85)"; + let currentFrame = beatmap.ScoringFrames[currentFrameIndex]; - let position = playfieldPosition(...hitObject.position); + if(currentFrame == null) + currentFrame = beatmap.ScoringFrames[beatmap.ScoringFrames.length - 1]; - if(options.fill){ - ctx.beginPath(); - ctx.fillStyle = hitObject.Color; - ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); - ctx.fill(); - } + const scoringFrames = []; - ctx.beginPath(); - ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.stroke(); + if(options.flashlight){ + ctx.globalAlpha = 1; - ctx.fillStyle = 'white'; - ctx.textBaseline = "middle"; - ctx.textAlign = "center"; + let { current } = getCursorAt(time, beatmap.ReplayInterpolated); - let fontSize = 16; - fontSize += 16 * (1 - (beatmap.CircleSize / 10)); + const { combo } = currentFrame; - fontSize *= scale_multiplier * sizeFactor; + let flIndex = 0; - ctx.font = `${fontSize}px sans-serif`; - ctx.fillText(hitObject.ComboNumber, ...position); - } - } - }); + if(combo >= 100) + flIndex = 1; + else if(combo >= 200) + flIndex = 2; - if(options.analyze){ - for(const hitObject of beatmap.hitObjects){ - if(hitObject.objectName == 'spinner') - continue; + const flImage = flImages[flIndex]; - if(hitObject.hitResult > 0 && hitObject.objectName == 'circle' - || hitObject.MissedSliderStart < 1 && hitObject.objectName == 'slider') - continue; + const cursorPos = playfieldPosition(current.x, current.y); - if(time < hitObject.startTime) - continue; + ctx.drawImage(flImage, cursorPos[0] - flImage.width / 2, cursorPos[1] - flImage.height / 2); - if(time - hitObject.startTime > 750) - continue; + const currentSlider = beatmap.hitObjects.find(a => time >= a.startTime && time < a.endTime && a.objectName === 'slider') - const position = playfieldPosition(...hitObject.position); + if(currentSlider){ + ctx.globalAlpha = 0.8; + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + } - ctx.globalAlpha = 1; - ctx.lineWidth = 3 * scale_multiplier; - ctx.strokeStyle = options.fill ? '#fa2f2f' : 'white'; - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.stroke(); - } - } + do{ + const newFrame = beatmap.ScoringFrames[previousFramesIndex]; - if(beatmap.ScoringFrames && beatmap.Replay.auto !== true){ - //const scoringFrames = getScoringFrames(time, beatmap.ScoringFrames); + if(newFrame == null) + break; - let previousFramesIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time - 5000); + if(newFrame.offset > time) + break; - let currentFrameIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time) - 1; + currentFrame = newFrame; - let currentFrame = beatmap.ScoringFrames[currentFrameIndex]; + scoringFrames.push(currentFrame); - if(currentFrame == null) - currentFrame = beatmap.ScoringFrames[beatmap.ScoringFrames.length - 1]; + previousFramesIndex++; + }while(currentFrame.offset < time) - const scoringFrames = []; + const UR_BAR_WIDTH = 160; + const UR_BAR_HEIGHT = 4; - if(options.flashlight){ - ctx.globalAlpha = 1; + const UR_BAR_Y = canvas.height - 35 - (15 * scale_multiplier); - let { current } = getCursorAt(time, beatmap.ReplayInterpolated); + const UR_BAR_100 = beatmap.HitWindow100 / beatmap.HitWindow50 * UR_BAR_WIDTH; + const UR_BAR_300 = beatmap.HitWindow300 / beatmap.HitWindow50 * UR_BAR_WIDTH; - const { combo } = currentFrame; + if(currentFrame != null){ + const comboPosition = [15, canvas.height - 35]; + const accuracyPosition = [canvas.width - 15, 40]; - let flIndex = 0; + ctx.fillStyle = "white"; + ctx.globalAlpha = 1; + ctx.textAlign = "left"; + ctx.textBaseline = "bottom"; + ctx.font = `${32 * scale_multiplier}px monospace`; + ctx.fillText(`${currentFrame.combo}x`, ...comboPosition); - if(combo >= 100) - flIndex = 1; - else if(combo >= 200) - flIndex = 2; + let { pp, stars } = currentFrame; - const flImage = flImages[flIndex]; + if(time - currentFrame.offset < 400 && scoringFrames.length > 1){ + let previousFrame; - const cursorPos = playfieldPosition(current.x, current.y); + for(let i = scoringFrames.length - 1; i > 0; i--){ + previousFrame = scoringFrames[i]; - ctx.drawImage(flImage, cursorPos[0] - flImage.width / 2, cursorPos[1] - flImage.height / 2); + if(previousFrame.offset <= time - 400 || previousFrame.pp !== currentFrame.pp) + break; + } - const currentSlider = beatmap.hitObjects.find(a => time >= a.startTime && time < a.endTime && a.objectName == 'slider') + const progress = (time - currentFrame.offset) / (time - previousFrame.offset); + const diffPP = currentFrame.pp - previousFrame.pp; + const diffStars = currentFrame.stars - previousFrame.stars; - if(currentSlider){ - ctx.globalAlpha = 0.8; - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } - } + pp = previousFrame.pp + diffPP * progress; + stars = previousFrame.stars + diffStars * progress; + } - do{ - const newFrame = beatmap.ScoringFrames[previousFramesIndex]; + ctx.textBaseline = "top"; + ctx.font = `${26 * scale_multiplier}px monospace`; + ctx.fillText(`${pp.toFixed(2)}pp`, 15, 45); - if(newFrame == null) - break; + ctx.font = `${21 * scale_multiplier}px monospace`; + ctx.fillText(`★${stars.toFixed(2)}`, 15, 47 + 26 * scale_multiplier); - if(newFrame.offset > time) - break; + let accuracy = 100; - currentFrame = newFrame; + const totalHits = currentFrame.count50 * 300 + currentFrame.count100 * 300 + currentFrame.count300 * 300 + currentFrame.countMiss * 300; - scoringFrames.push(currentFrame); + if(totalHits > 0) + accuracy = (currentFrame.count50 * 50 + currentFrame.count100 * 100 + currentFrame.count300 * 300) + / totalHits * 100; - previousFramesIndex++; - }while(currentFrame.offset < time) + ctx.textAlign = "right"; + ctx.textBaseline = "top"; + ctx.font = `${26 * scale_multiplier}px monospace`; + ctx.fillText(`${accuracy.toFixed(2)}%`, ...accuracyPosition); - const UR_BAR_WIDTH = 160; - const UR_BAR_HEIGHT = 4; + const hitCountPosition = [canvas.width - 15, 45 + 26 * scale_multiplier]; - const UR_BAR_Y = canvas.height - 35 - (15 * scale_multiplier); + ctx.font = `${21 * scale_multiplier}px monospace`; + ctx.fillText(`${currentFrame.count100}x100 ${currentFrame.count50}x50`, ...hitCountPosition); - const UR_BAR_100 = beatmap.HitWindow100 / beatmap.HitWindow50 * UR_BAR_WIDTH; - const UR_BAR_300 = beatmap.HitWindow300 / beatmap.HitWindow50 * UR_BAR_WIDTH; + hitCountPosition[1] += 2 + 21 * scale_multiplier; + ctx.fillText(`${currentFrame.countMiss}xMiss`, ...hitCountPosition); - if(currentFrame != null){ - const comboPosition = [15, canvas.height - 35]; - const accuracyPosition = [canvas.width - 15, 40]; + const urPosition = [canvas.width - 15, canvas.height - 35]; - ctx.fillStyle = "white"; - ctx.globalAlpha = 1; - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; - ctx.font = `${32 * scale_multiplier}px monospace`; - ctx.fillText(`${currentFrame.combo}x`, ...comboPosition); + ctx.textBaseline = "bottom"; + ctx.font = `${26 * scale_multiplier}px monospace`; - let { pp, stars } = currentFrame; + let urText = 'UR'; + let { ur } = currentFrame; - if(time - currentFrame.offset < 400 && scoringFrames.length > 1){ - let previousFrame; + if(beatmap.Replay && (beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC') || beatmap.Replay.Mods.includes("HT"))){ + urText = 'cvUR'; - for(let i = scoringFrames.length - 1; i > 0; i--){ - previousFrame = scoringFrames[i]; + if(beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC')) + ur /= 1.5; - if(previousFrame.offset <= time - 400 || previousFrame.pp != currentFrame.pp) - break; - } + if(beatmap.Replay.Mods.includes('HT')) + ur /= 0.75; + } - const progress = (time - currentFrame.offset) / (time - previousFrame.offset); - const diffPP = currentFrame.pp - previousFrame.pp; - const diffStars = currentFrame.stars - previousFrame.stars; + ctx.fillText(`${ur.toFixed(2)} ${urText}`, ...urPosition); - pp = previousFrame.pp + diffPP * progress; - stars = previousFrame.stars + diffStars * progress; - } + /* + ctx.textAlign = "right"; + ctx.fillText(`${time}`, canvas.width - 15, canvas.height - 35); + ctx.fillText(`${currentFrame.offset}`, canvas.width - 15, canvas.height - 65);*/ - ctx.textBaseline = "top"; - ctx.font = `${26 * scale_multiplier}px monospace`; - ctx.fillText(`${pp.toFixed(2)}pp`, 15, 45); + ctx.globalAlpha = 0.5; - ctx.font = `${21 * scale_multiplier}px monospace`; - ctx.fillText(`★${stars.toFixed(2)}`, 15, 47 + 26 * scale_multiplier); - - let accuracy = 100; + ctx.fillStyle = '#ff9100'; + ctx.fillRect(canvas.width / 2 - UR_BAR_WIDTH / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_WIDTH, UR_BAR_HEIGHT); - const totalHits = currentFrame.count50 * 300 + currentFrame.count100 * 300 + currentFrame.count300 * 300 + currentFrame.countMiss * 300; + ctx.fillStyle = '#4dff00'; + ctx.fillRect(canvas.width / 2 - UR_BAR_100 / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_100, UR_BAR_HEIGHT); - if(totalHits > 0) - accuracy = (currentFrame.count50 * 50 + currentFrame.count100 * 100 + currentFrame.count300 * 300) - / totalHits * 100; + ctx.fillStyle = '#00e5ff'; + ctx.fillRect(canvas.width / 2 - UR_BAR_300 / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_300, UR_BAR_HEIGHT); - ctx.textAlign = "right"; - ctx.textBaseline = "top"; - ctx.font = `${26 * scale_multiplier}px monospace`; - ctx.fillText(`${accuracy.toFixed(2)}%`, ...accuracyPosition); + ctx.globalAlpha = 1; - const hitCountPosition = [canvas.width - 15, 45 + 26 * scale_multiplier]; + ctx.textAlign = "left"; + ctx.textBaseline = "bottom"; + ctx.font = `${16 * scale_multiplier}px sans-serif`; - ctx.font = `${21 * scale_multiplier}px monospace`; - ctx.fillText(`${currentFrame.count100}x100 ${currentFrame.count50}x50`, ...hitCountPosition); + ctx.fillStyle = 'rgb(255,255,255,0.8)'; - hitCountPosition[1] += 2 + 21 * scale_multiplier; - ctx.fillText(`${currentFrame.countMiss}xMiss`, ...hitCountPosition); + ctx.fillText('W.I.P. – scoring not accurate yet', 15, canvas.height - 10); + } - const urPosition = [canvas.width - 15, canvas.height - 35]; + for(const scoringFrame of scoringFrames){ + if(scoringFrame.hitOffset != null){ + switch(scoringFrame.result){ + case 300: + ctx.fillStyle = '#00e5ff'; + break; + case 100: + ctx.fillStyle = '#4dff00'; + break; + case 50: + ctx.fillStyle = '#ff9100'; + break; + default: + ctx.fillStyle = 'transparent'; + } - ctx.textBaseline = "bottom"; - ctx.font = `${26 * scale_multiplier}px monospace`; + ctx.globalAlpha = 0.35; - let urText = 'UR'; - let { ur } = currentFrame; + if(time - scoringFrame.offset > 4000) + ctx.globalAlpha *= Math.max(0, 1 - (time - (scoringFrame.offset + 4000)) / 1000); - if(beatmap.Replay && (beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC') || beatmap.Replay.Mods.includes("HT"))){ - urText = 'cvUR'; + let posX = canvas.width / 2; - if(beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC')) - ur /= 1.5; + const offsetX = Math.abs(scoringFrame.hitOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); - if(beatmap.Replay.Mods.includes('HT')) - ur /= 0.75; - } + if(scoringFrame.hitOffset > 0) + posX += offsetX; + else + posX -= offsetX; - ctx.fillText(`${ur.toFixed(2)} ${urText}`, ...urPosition); + ctx.fillRect(posX, UR_BAR_Y - 16 / 2, 2, 16); + } - /* - ctx.textAlign = "right"; - ctx.fillText(`${time}`, canvas.width - 15, canvas.height - 35); - ctx.fillText(`${currentFrame.offset}`, canvas.width - 15, canvas.height - 65);*/ + if(!(['miss', 50, 100].includes(scoringFrame.result))) + continue; - ctx.globalAlpha = 0.5; + if(time - scoringFrame.offset > 750) + continue; - ctx.fillStyle = '#ff9100'; - ctx.fillRect(canvas.width / 2 - UR_BAR_WIDTH / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_WIDTH, UR_BAR_HEIGHT); + ctx.globalAlpha = Math.min(1, 1.5 - (time - scoringFrame.offset) / 750); + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.font = `${30 * scale_multiplier}px sans-serif`; - ctx.fillStyle = '#4dff00'; - ctx.fillRect(canvas.width / 2 - UR_BAR_100 / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_100, UR_BAR_HEIGHT); + const position = scoringFrame.position.slice(); - ctx.fillStyle = '#00e5ff'; - ctx.fillRect(canvas.width / 2 - UR_BAR_300 / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_300, UR_BAR_HEIGHT); + if(scoringFrame.result == 'miss'){ + position[1] += (time - scoringFrame.offset) / 750 * 35; - ctx.globalAlpha = 1; + ctx.fillStyle = "#f56767"; - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; - ctx.font = `${16 * scale_multiplier}px sans-serif`; + ctx.fillText('X', ...playfieldPosition(...position)); + continue; + } - ctx.fillStyle = 'rgb(255,255,255,0.8)'; + if(scoringFrame.result == 50){ + ctx.fillStyle = "#67b5f5"; - ctx.fillText('W.I.P. – scoring not accurate yet', 15, canvas.height - 10); - } + ctx.fillText('50', ...playfieldPosition(...position)); + continue; + } - for(const scoringFrame of scoringFrames){ - if(scoringFrame.hitOffset != null){ - switch(scoringFrame.result){ - case 300: - ctx.fillStyle = '#00e5ff'; - break; - case 100: - ctx.fillStyle = '#4dff00'; - break; - case 50: - ctx.fillStyle = '#ff9100'; - break; - default: - ctx.fillStyle = 'transparent'; - } + if(scoringFrame.result == 100){ + ctx.fillStyle = "#67f575"; - ctx.globalAlpha = 0.35; + ctx.fillText('100', ...playfieldPosition(...position)); + continue; + } + } - if(time - scoringFrame.offset > 4000) - ctx.globalAlpha *= Math.max(0, 1 - (time - (scoringFrame.offset + 4000)) / 1000); + let scoringFrameOffsets = scoringFrames.filter(a => a.hitOffset != null).map(a => a.hitOffset); - let posX = canvas.width / 2; + const avgOffset = scoringFrameOffsets.length > 0 ? scoringFrameOffsets.reduce((a, v, i) => (a * i + v) / (i + 1)) : 0; - const offsetX = Math.abs(scoringFrame.hitOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); + let posX = canvas.width / 2; - if(scoringFrame.hitOffset > 0) - posX += offsetX; - else - posX -= offsetX; + const offsetX = Math.abs(avgOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); - ctx.fillRect(posX, UR_BAR_Y - 16 / 2, 2, 16); - } + if(avgOffset > 0) + posX += offsetX; + else + posX -= offsetX; - if(!(['miss', 50, 100].includes(scoringFrame.result))) - continue; + ctx.globalAlpha = 1; + ctx.fillStyle = 'white'; - if(time - scoringFrame.offset > 750) - continue; + ctx.beginPath(); + ctx.moveTo(posX - 5, UR_BAR_Y - 16 / 2); + ctx.lineTo(posX, UR_BAR_Y - 16 / 2 + 7); + ctx.lineTo(posX + 5, UR_BAR_Y - 16 / 2); + ctx.fill(); - ctx.globalAlpha = Math.min(1, 1.5 - (time - scoringFrame.offset) / 750); - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.font = `${30 * scale_multiplier}px sans-serif`; + ctx.fillRect(canvas.width / 2 - 1, UR_BAR_Y - 16 / 2, 2, 16); + } - const position = scoringFrame.position.slice(); + // Draw replay cursor + if(beatmap.Replay){ + let replay_point = getCursorAt(time, beatmap.ReplayInterpolated); - if(scoringFrame.result == 'miss'){ - position[1] += (time - scoringFrame.offset) / 750 * 35; - - ctx.fillStyle = "#f56767"; + let smokeActive = false; - ctx.fillText('X', ...playfieldPosition(...position)); - continue; - } + ctx.globalAlpha = 1; - if(scoringFrame.result == 50){ - ctx.fillStyle = "#67b5f5"; + for(let i = beatmap.Replay.lastCursor - 1; i > 0; i--){ + const frame = beatmap.Replay.replay_data[i]; + const previousFrame = beatmap.Replay.replay_data[i - 1]; - ctx.fillText('50', ...playfieldPosition(...position)); - continue; - } + if(frame.offset > time) + continue; - if(scoringFrame.result == 100){ - ctx.fillStyle = "#67f575"; + if(time - frame.offset > 5000) + break; - ctx.fillText('100', ...playfieldPosition(...position)); - continue; - } - } + ctx.lineWidth = 1; + ctx.strokeStyle = "rgba(255,255,255,0.7)"; - let scoringFrameOffsets = scoringFrames.filter(a => a.hitOffset != null).map(a => a.hitOffset); + if(options.analyze && previousFrame != null && time - frame.offset < 750){ + const position0 = playfieldPosition(previousFrame.x, previousFrame.y); + const position1 = playfieldPosition(frame.x, frame.y); - const avgOffset = scoringFrameOffsets.length > 0 ? scoringFrameOffsets.reduce((a, v, i) => (a * i + v) / (i + 1)) : 0; + ctx.beginPath(); - let posX = canvas.width / 2; + ctx.moveTo(...position0); + ctx.lineTo(...position1); - const offsetX = Math.abs(avgOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); + ctx.stroke(); + } - if(avgOffset > 0) - posX += offsetX; - else - posX -= offsetX; + if(options.analyze && previousFrame != null && time - frame.offset < 750){ + if(((frame.K1 || frame.M1) && !previousFrame.K1 && !previousFrame.M1) + ||((frame.K2 || frame.M2) && !previousFrame.K2 && !previousFrame.M2)){ - ctx.globalAlpha = 1; - ctx.fillStyle = 'white'; + ctx.strokeStyle = "white"; - ctx.beginPath(); - ctx.moveTo(posX - 5, UR_BAR_Y - 16 / 2); - ctx.lineTo(posX, UR_BAR_Y - 16 / 2 + 7); - ctx.lineTo(posX + 5, UR_BAR_Y - 16 / 2); - ctx.fill(); + const position = playfieldPosition(frame.x, frame.y); - ctx.fillRect(canvas.width / 2 - 1, UR_BAR_Y - 16 / 2, 2, 16); - } + ctx.beginPath(); - // Draw replay cursor - if(beatmap.Replay){ - let replay_point = getCursorAt(time, beatmap.ReplayInterpolated); + ctx.moveTo(position[0], position[1] - 5); + ctx.lineTo(position[0], position[1] + 5); + ctx.stroke(); - let smokeActive = false; + ctx.moveTo(position[0] - 5, position[1]); + ctx.lineTo(position[0] + 5, position[1]); + ctx.stroke(); + } + } - ctx.globalAlpha = 1; + ctx.lineWidth = 6 * scale_multiplier; + ctx.strokeStyle = "rgba(255,255,255,0.4)"; - for(let i = beatmap.Replay.lastCursor - 1; i > 0; i--){ - const frame = beatmap.Replay.replay_data[i]; - const previousFrame = beatmap.Replay.replay_data[i - 1]; + if(frame.S === false && smokeActive){ + if(smokeActive && !options.analyze){ + ctx.stroke(); + smokeActive = false; + } - if(frame.offset > time) - continue; + continue; + } - if(time - frame.offset > 5000) - break; + if(frame.S){ + if(!smokeActive){ + smokeActive = true; + ctx.beginPath(); + ctx.moveTo(...playfieldPosition(frame.x, frame.y)); + }else{ + ctx.lineTo(...playfieldPosition(frame.x, frame.y)); + } + } + } - ctx.lineWidth = 1; - ctx.strokeStyle = "rgba(255,255,255,0.7)"; + if(smokeActive && !options.analyze){ + ctx.stroke(); + } - if(options.analyze && previousFrame != null && time - frame.offset < 750){ - const position0 = playfieldPosition(previousFrame.x, previousFrame.y); - const position1 = playfieldPosition(frame.x, frame.y); + if(replay_point){ + if(beatmap.Replay.auto !== true){ + ctx.globalAlpha = 1; - ctx.beginPath(); + const { K1, K2, M1, M2 } = replay_point.current; - ctx.moveTo(...position0); - ctx.lineTo(...position1); + const keyOverlayTop = canvas.height / 2 - (KEY_OVERLAY_SIZE * 4 + KEY_OVERLAY_PADDING * 4) / 2; - ctx.stroke(); - } + ctx.fillStyle = K1 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; + ctx.fillRect(canvas.width - 30, keyOverlayTop, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); - if(options.analyze && previousFrame != null && time - frame.offset < 750){ - if(((frame.K1 || frame.M1) && !previousFrame.K1 && !previousFrame.M1) - ||((frame.K2 || frame.M2) && !previousFrame.K2 && !previousFrame.M2)){ + ctx.fillStyle = K2 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; + ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE + KEY_OVERLAY_PADDING, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); - ctx.strokeStyle = "white"; + ctx.fillStyle = M1 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; + ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE * 2 + KEY_OVERLAY_PADDING * 2, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); - const position = playfieldPosition(frame.x, frame.y); + ctx.fillStyle = M2 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; + ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE * 3 + KEY_OVERLAY_PADDING * 3, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); + } - ctx.beginPath(); + if(Array.isArray(replay_point.previous) && !options.analyze){ + ctx.globalAlpha = .35; - ctx.moveTo(position[0], position[1] - 5); - ctx.lineTo(position[0], position[1] + 5); - ctx.stroke(); + ctx.beginPath(); - ctx.moveTo(position[0] - 5, position[1]); - ctx.lineTo(position[0] + 5, position[1]); - ctx.stroke(); - } - } + for(const [index, previousFrame] of replay_point.previous.entries()){ + let position = playfieldPosition(previousFrame.x, previousFrame.y); - ctx.lineWidth = 6 * scale_multiplier; - ctx.strokeStyle = "rgba(255,255,255,0.4)"; + if(index === 0) + ctx.moveTo(...position); + else + ctx.lineTo(...position); + } - if(frame.S == false && smokeActive){ - if(smokeActive && !options.analyze){ - ctx.stroke(); - smokeActive = false; - } - - continue; - } + ctx.lineWidth = 13 * scale_multiplier; + ctx.lineCap = "round"; - if(frame.S){ - if(!smokeActive){ - smokeActive = true; - ctx.beginPath(); - ctx.moveTo(...playfieldPosition(frame.x, frame.y)); - }else{ - ctx.lineTo(...playfieldPosition(frame.x, frame.y)); - } - } - } + if(options.fill) + ctx.strokeStyle = '#fff4ab'; + else + ctx.strokeStyle = 'white'; - if(smokeActive && !options.analyze){ - ctx.stroke(); - } + ctx.stroke(); + } - if(replay_point){ - if(beatmap.Replay.auto !== true){ - ctx.globalAlpha = 1; + if(options.fill) + ctx.fillStyle = '#fff460'; + else + ctx.fillStyle = 'white'; - const { K1, K2, M1, M2 } = replay_point.current; + let { current } = replay_point; - const keyOverlayTop = canvas.height / 2 - (KEY_OVERLAY_SIZE * 4 + KEY_OVERLAY_PADDING * 4) / 2; + let position = playfieldPosition(current.x, current.y); - ctx.fillStyle = K1 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; - ctx.fillRect(canvas.width - 30, keyOverlayTop, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); + ctx.globalAlpha = 1; - ctx.fillStyle = K2 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; - ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE * 1 + KEY_OVERLAY_PADDING * 1, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * 13, 0, 2 * Math.PI, false); + ctx.fill(); + } + } - ctx.fillStyle = M1 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; - ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE * 2 + KEY_OVERLAY_PADDING * 2, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); + // Draw playfield border + if(options.border){ + ctx.strokeStyle = "rgb(200,200,200)"; + ctx.lineWidth = 1; + ctx.globalAlpha = 1; - ctx.fillStyle = M2 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; - ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE * 3 + KEY_OVERLAY_PADDING * 3, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); - } - - if(Array.isArray(replay_point.previous) && !options.analyze){ - ctx.globalAlpha = .35; + let position = playfieldPosition(0, 0); + let size = playfieldPosition(PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT); + ctx.strokeRect(...position, size[0] - position[0], size[1] - position[1]); + } + } - ctx.beginPath(); - - for(const [index, previousFrame] of replay_point.previous.entries()){ - let position = playfieldPosition(previousFrame.x, previousFrame.y); + let time = start_time; - if(index == 0) - ctx.moveTo(...position); - else - ctx.lineTo(...position); - } + prepareCanvas(size); + //preprocessSliders(); - ctx.lineWidth = 13 * scale_multiplier; - ctx.lineCap = "round"; + beatmap.ReplayInterpolated = interpolateReplayData(beatmap.Replay); - if(options.fill) - ctx.strokeStyle = '#fff4ab'; - else - ctx.strokeStyle = 'white'; - - ctx.stroke(); - } - - if(options.fill) - ctx.fillStyle = '#fff460'; - else - ctx.fillStyle = 'white'; - - let { current } = replay_point; - - let position = playfieldPosition(current.x, current.y); - - ctx.globalAlpha = 1; - - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * 13, 0, 2 * Math.PI, false); - ctx.fill(); - } - } - - // Draw playfield border - if(options.border){ - ctx.strokeStyle = "rgb(200,200,200)"; - ctx.lineWidth = 1; - ctx.globalAlpha = 1; - - let position = playfieldPosition(0, 0); - let size = playfieldPosition(PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT); - ctx.strokeRect(...position, size[0] - position[0], size[1] - position[1]); - } - } - - let time = start_time; - - prepareCanvas(size); - //preprocessSliders(); - - beatmap.ReplayInterpolated = interpolateReplayData(beatmap.Replay); - - for(i in images){ - let image_path = images[i]; - - images[i] = await new Promise((resolve, reject) => { - let img = new Image(); - img.onload = () => { - resolve(img); - }; - - img.onerror = reject; - img.src = image_path; - }); - } + for(let i in images){ + let image_path = images[i]; + + images[i] = await new Promise((resolve, reject) => { + let img = new Image(); + img.onload = () => { + resolve(img); + }; + + img.onerror = reject; + img.src = image_path; + }); + } + + if(end_time){ + while(time < end_time){ + while (WAITING_FOR_SERVER_ACK){ + await new Promise(r => setTimeout(r, 20)); + } + + + processFrame(time, options); + + let image_data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + + // Convert rgb with alpha values to pure rgb as gif doesn't support alpha + if(options.type === 'gif'){ + for(let i = 0; i < image_data.length; i += 4){ + if(image_data[i + 3] > 0){ + let scale = Math.round(image_data[i] * image_data[i + 3] / 255); + image_data[i] = scale; + image_data[i + 1] = scale; + image_data[i + 2] = scale; + image_data[i + 3] = 255; + } + } + } + + // await fs.writeFile(path.resolve(file_path, `${current_frame}.rgba`), Buffer.from(image_data)); + + // process.send(current_frame); + let abuf = Buffer.from(image_data); + + // process.send({ + // worker_id: worker_id, + // data: abuf + // }); + + log_as_worker("Sending frame data to main thread " + ++frame_counter); + for (let i = 0; i < Math.ceil(abuf.length/32000); i++){ + ipc.of.world.emit('app.framedata', { + worker_id: worker_id, + seqno: i, + last: (i + 1) * 32000 >= abuf.length, + frame_data: abuf.slice(i * 32000, (i+1)*32000).toString('base64') + }); + } + WAITING_FOR_SERVER_ACK = true; + + current_frame += threads; + time += time_frame; + } + + ipc.of.world.emit('app.readyToTerminate', 'true'); + }else{ // no end time -> render single frame + processFrame(time, options); + + process.send(canvas.toBuffer().toString('base64')); + process.exit(0); + } + } + + } + ) + } +); + + + + +let images = { + "arrow": path.resolve(resources, "images", "arrow.svg") +}; - if(end_time){ - while(time < end_time){ - processFrame(time, options); +// process.on('uncaughtException', err => { +// helper.error(err); +// process.exit(1); +// }); - let image_data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; - // Convert rgb with alpha values to pure rgb as gif doesn't support alpha - if(options.type == 'gif'){ - for(let i = 0; i < image_data.length; i += 4){ - if(image_data[i + 3] > 0){ - let scale = Math.round(image_data[i + 0] * image_data[i + 3] / 255); - image_data[i] = scale; - image_data[i + 1] = scale; - image_data[i + 2] = scale; - image_data[i + 3] = 255; - } - } - } - - await fs.writeFile(path.resolve(file_path, `${current_frame}.rgba`), Buffer.from(image_data)); - - process.send(current_frame); - - current_frame += threads; - time += time_frame; - } - - process.exit(0); - }else{ - processFrame(time, options); - - process.send(canvas.toBuffer().toString('base64')); - process.exit(0); - } -}); From 7d8cc5549cdb72958d8f2a58652e13d0c1d072ec Mon Sep 17 00:00:00 2001 From: daohe Date: Thu, 15 Jul 2021 13:21:19 +0100 Subject: [PATCH 2/9] add render throttling in case the video encoder doesn't keep up with the speed at which frame render workers render frames, it would lead to overrunning memory. this throttling ensures that there are only a couple frames kept in memory as buffer (cherry picked from commit 6e1e47d6acbd8d13dcdc53b5500a3d1508512865) --- renderer/render_frame.js | 97 ++++++++++++++++++++++++--------------- renderer/render_worker.js | 8 ++-- 2 files changed, 63 insertions(+), 42 deletions(-) diff --git a/renderer/render_frame.js b/renderer/render_frame.js index 63555b8..e394935 100644 --- a/renderer/render_frame.js +++ b/renderer/render_frame.js @@ -16,6 +16,8 @@ const ffmpeg = require('ffmpeg-static'); const unzip = require('unzipper'); const disk = require('diskusage'); +const EventEmitter = require('events'); + const {execFile, fork, spawn} = require('child_process'); const config = require('../config.json'); @@ -126,7 +128,7 @@ async function renderHitsounds(mediaPromise, beatmap, start_time, actual_length, let beatmapAudio = false; try { - helper.log(start_time) + // helper.log(start_time) await execFilePromise(ffmpeg, [ '-ss', start_time / 1000, '-i', `"${media.audio_path}"`, '-t', actual_length * Math.max(1, time_scale) / 1000, '-filter:a', `"afade=t=out:st=${Math.max(0, actual_length * time_scale / 1000 - 0.5 / time_scale)}:d=0.5,atempo=${time_scale},volume=0.7"`, @@ -474,11 +476,32 @@ module.exports = { if (config.debug) console.time('process beatmap'); - function Queue() { this.frames = []; } - Queue.prototype.enqueue = function (frame){ this.frames.push(frame); } - Queue.prototype.dequeue = function () { return this.frames.shift(); } - Queue.prototype.isEmpty = function () { return this.frames.length === 0; } - Queue.prototype.peek = function () { return !this.isEmpty() ? this.frames[0] : undefined; } + class Queue extends EventEmitter { + constructor(){ + super(); + this.frames = []; + this.emitEvents = false; + this.throttleThreshold = 3; + } + + enableEvents(){this.emitEvents = true;} + disableEvents(){this.emitEvents = false;} + + enqueue(frame){ + this.frames.push(frame); + } + dequeue() { + if (this.frames.length - 1 < this.throttleThreshold && this.emitEvents){ + this.emit('unthrottle'); + } + return this.frames.shift(); + } + isEmpty() { return this.frames.length === 0; } + length() { return this.frames.length; } + peek() { return !this.isEmpty() ? this.frames[0] : undefined; } + shouldSlowdown() { return this.length() > 1}; + } + const {msg} = options; @@ -650,17 +673,12 @@ module.exports = { file_path = path.resolve(config.frame_path != null ? config.frame_path : os.tmpdir(), 'frames', `${rnd}`); - await fs.promises.mkdir(file_path, {recursive: true}); let threads = require('os').cpus().length; - // let threads = 1; - let modded_length = time_scale > 1 ? Math.min(actual_length * time_scale, lastObjectTime) : actual_length; - let amount_frames = Math.floor(modded_length / time_frame); - // let frames_size = amount_frames * size[0] * size[1] * 4; // recursively does reads frame from file and pipes to 2nd ffmpeg stdin let pipeFrameLoop = (ffmpegProcess, cb) => { @@ -699,43 +717,42 @@ module.exports = { } let newPipeFrameLoop = async (ffmpegProcess, callback) => { - helper.log("hello? somebody there?") + // helper.log("################### entering newPipeFrameLoop #######################"); let next_frame_worker_id = 0; let frame_counter = 0; while (frame_counter < amount_frames) { - helper.log("trying to feed frame " + frame_counter); let next_frame_worker_queue = worker_frame_queues[next_frame_worker_id]; let next_frame = next_frame_worker_queue.peek(); if (typeof next_frame === 'undefined') { - await new Promise(r => setTimeout(r, 10)); + // helper.log("queue empty... sleeping (waiting on " + next_frame_worker_id + " - video frame #" + frame_counter +")"); + // helper.log("waiting on next frame, sleeping..."); + await new Promise(r => setTimeout(r, 50)); continue; } - // helper.log("in newPipeFrameLoop sending frames to ffmpeg") // let next_frame_buffer = Buffer.from(next_frame); - let next_frame_buffer = Buffer.from(next_frame); - helper.log(next_frame_buffer.length); - ffmpegProcess.stdin.write(next_frame_buffer, err => { + // helper.log(next_frame_buffer.length); + ffmpegProcess.stdin.write(next_frame, err => { if (err) { - helper.error("something bad happened"); + helper.error("something bad happened on video encoder"); helper.error(err); callback(null); - return; } - // helper.log("in the callback, but no err"); - frame_counter++; + }); - if (frame_counter === amount_frames){ - ffmpegProcess.stdin.end(); - callback(null); - return; - } + frame_counter++; - next_frame_worker_id = (next_frame_worker_id + 1) % threads; - next_frame_worker_queue.dequeue(); - }); + if (frame_counter === amount_frames){ + ffmpegProcess.stdin.end(); + callback(null); + return; + } - await new Promise(r => setTimeout(r, 5)); + next_frame_worker_id = (next_frame_worker_id + 1) % threads; + next_frame_worker_queue.dequeue(); + // helper.log("end of loop: " + next_frame_worker_id, worker_frame_queues); + + // await new Promise(r => setTimeout(r, 5)); } } @@ -864,7 +881,7 @@ module.exports = { if (!line.startsWith('frame=')) return; - helper.log(line); + // helper.log(line); const frame = parseInt(line.substring(6).trim()); renderStatus[2] = `– encoding video (${Math.round(frame / amount_frames * 100)}%)`; @@ -933,6 +950,11 @@ module.exports = { let worker_to_init = workers[worker_idx]; worker_frame_queues[worker_idx] = new Queue(); + worker_frame_queues[worker_idx].enableEvents(); + worker_frame_queues[worker_idx].on('unthrottle', function(){ + ipc.server.emit(socket, 'app.resumeWorking', 'true'); + }); + worker_frame_buffers[worker_idx] = []; // init frame buffer ipc.server.emit(socket, 'receiveWork', { @@ -975,15 +997,14 @@ module.exports = { // todo: implement slowing down of frame tap if video encoder can't keep up let frame_wid = data.worker_id; worker_frame_buffers[frame_wid].push(Buffer.from(data.frame_data, 'base64')); + ipc.server.emit(socket, 'app.framedataAck', 'ACK'); if(data.last){ - ipc.server.emit(socket, 'app.framedataFullAck', 'ACK'); - log_as_server("received full frame from worker " + frame_wid + ", received in total: " + ++frame_counter); - + // log_as_server("received full frame from worker " + frame_wid + ", received in total: " + ++frame_counter + ", video frame #" + data.video_frame_seqno); worker_frame_queues[frame_wid].enqueue(Buffer.concat(worker_frame_buffers[frame_wid])) worker_frame_buffers[frame_wid].splice(0, worker_frame_buffers[frame_wid].length); - } - else { - ipc.server.emit(socket, 'app.framedataAck', 'ACK'); + if(worker_frame_queues[frame_wid].length() < worker_frame_queues[frame_wid].throttleThreshold){ + ipc.server.emit(socket, 'app.resumeWorking', 'true'); + } } }) diff --git a/renderer/render_worker.js b/renderer/render_worker.js index 6dd7b32..494d18c 100644 --- a/renderer/render_worker.js +++ b/renderer/render_worker.js @@ -82,11 +82,10 @@ ipc.connectTo( ipc.of.world.on('app.framedataAck', function(data){ // log_as_worker("Received ACK from server"); - // WAITING_FOR_SERVER_ACK = false; }) - ipc.of.world.on('app.framedataFullAck', function(data){ - // log_as_worker("Received ACK from server"); + ipc.of.world.on('app.resumeWorking', function(data){ + // log_as_worker("Received ACK from server to resume work"); WAITING_FOR_SERVER_ACK = false; }) @@ -1278,11 +1277,12 @@ ipc.connectTo( // data: abuf // }); - log_as_worker("Sending frame data to main thread " + ++frame_counter); + // log_as_worker("Sending frame data to main thread " + ++frame_counter); for (let i = 0; i < Math.ceil(abuf.length/32000); i++){ ipc.of.world.emit('app.framedata', { worker_id: worker_id, seqno: i, + video_frame_seqno: current_frame, last: (i + 1) * 32000 >= abuf.length, frame_data: abuf.slice(i * 32000, (i+1)*32000).toString('base64') }); From fc2f3d939e6cc3cf6c936cd834ee2439ca41dba0 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Sat, 25 Dec 2021 08:10:43 +0000 Subject: [PATCH 3/9] update render progress embed contents --- renderer/render_frame.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/renderer/render_frame.js b/renderer/render_frame.js index e394935..e08ecc9 100644 --- a/renderer/render_frame.js +++ b/renderer/render_frame.js @@ -507,7 +507,7 @@ module.exports = { options.msg = null; - const renderStatus = ['– processing beatmap', '– rendering frames', '– encoding video']; + const renderStatus = ['– processing beatmap', '– rendering video']; // noinspection JSCheckFunctionSignatures const renderMessage = await msg.channel.send({embed: {description: renderStatus.join("\n")}}); @@ -796,7 +796,7 @@ module.exports = { if (config.debug) console.timeEnd('encode video'); - renderStatus[1] = `✓ encoding video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; + // renderStatus[1] = `✓ encoding video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; resolveRender({ files: [{ @@ -863,7 +863,7 @@ module.exports = { if (config.debug) console.timeEnd('encode video'); - renderStatus[2] = `✓ encoding video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; + renderStatus[1] = `✓ rendering video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; resolveRender({ files: [{ @@ -884,7 +884,7 @@ module.exports = { // helper.log(line); const frame = parseInt(line.substring(6).trim()); - renderStatus[2] = `– encoding video (${Math.round(frame / amount_frames * 100)}%)`; + renderStatus[1] = `– rendering video (${Math.round(frame / amount_frames * 100)}%)`; }); newPipeFrameLoop(ffmpegProcess, err => { @@ -980,7 +980,7 @@ module.exports = { done++; if (done === threads) { - renderStatus[1] = `✓ rendering frames (${((Date.now() - framesProcessStart) / 1000).toFixed(3)}s)`; + // renderStatus[1] = `✓ rendering frames (${((Date.now() - framesProcessStart) / 1000).toFixed(3)}s)`; if (config.debug) console.timeEnd('render beatmap'); From 051f099b9b689009ad3162e77194771717d5baed Mon Sep 17 00:00:00 2001 From: ILW8 Date: Sat, 25 Dec 2021 09:59:19 +0000 Subject: [PATCH 4/9] fix renderer worker indexing issue on node >14 --- renderer/render_frame.js | 1 + 1 file changed, 1 insertion(+) diff --git a/renderer/render_frame.js b/renderer/render_frame.js index e08ecc9..524145e 100644 --- a/renderer/render_frame.js +++ b/renderer/render_frame.js @@ -984,6 +984,7 @@ module.exports = { if (config.debug) console.timeEnd('render beatmap'); + ipc.server.stop(); } }); From 626d7dc028de8cf158a08351555ded901d806316 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Tue, 28 Dec 2021 03:11:25 +0000 Subject: [PATCH 5/9] fix single-frame render issue with render_worker temporarily re-adds `process.on` to handle single-frame renders, render_frame.js hasn't implemented using node-ipc for worker communication when rendering a single frame --- renderer/render_worker.js | 1925 ++++++++++++++++++------------------- 1 file changed, 958 insertions(+), 967 deletions(-) diff --git a/renderer/render_worker.js b/renderer/render_worker.js index 494d18c..a0e2f8e 100644 --- a/renderer/render_worker.js +++ b/renderer/render_worker.js @@ -19,1304 +19,1295 @@ const resources = path.resolve(__dirname, "res"); const ipc = require('node-ipc'); const crypto = require("crypto"); - - - - let WAITING_FOR_SERVER_ACK = false; let frame_counter = 0; - - - - - +let images = { + "arrow": path.resolve(resources, "images", "arrow.svg") +}; let worker_id = crypto.randomBytes(3*4).toString('hex'); let log_as_worker = function(message) { helper.log("[worker " + worker_id + "] ", message); } +async function run_worker_job(data) { + let { beatmap, start_time, end_time, time_frame, file_path, options, threads, current_frame, size } = data; + + function resize(){ + active_playfield_width = canvas.width * 0.7; + active_playfield_height = active_playfield_width * (3/4); + let position = playfieldPosition(0, 0); + let size = playfieldPosition(PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT); + scale_multiplier = (size[0] - position[0]) / PLAYFIELD_WIDTH; + } -ipc.config.id = 'worker-' + worker_id; -ipc.config.retry= 200; -ipc.config.sync = true; -// ipc.config.rawBuffer=true; -// ipc.config.encoding ='base64'; -ipc.config.logger = log_as_worker -ipc.config.silent = true; + // Convert osu pixels position to canvas coordinates + function playfieldPosition(x, y){ + let ratio_x = x / PLAYFIELD_WIDTH; + let ratio_y = y / PLAYFIELD_HEIGHT; -ipc.connectTo( - 'world', - ipc.config.socketRoot + ipc.config.appspace + 'world', - function(){ - // ipc.of.world.on( - // 'connect', - // function(){ - // //make a 6 byte buffer for example - // const myBuffer=Buffer.alloc(6).fill(0); - // - // myBuffer.writeUInt16BE(0x02,0); - // myBuffer.writeUInt32BE(0xffeecc,2); - // - // ipc.log('## connected to world ##', ipc.config.delay); - // ipc.of.world.emit( - // myBuffer - // ); - // } - // ); + return [ + active_playfield_width * ratio_x + canvas.width * 0.15, + active_playfield_height * ratio_y + canvas.height * 0.15 + ]; + } - ipc.of.world.on( - 'connect', - function(){ - ipc.log('## connected to world ##', ipc.config.delay); - ipc.of.world.emit('workerReady', worker_id); - } - ); + const flImages = []; - ipc.of.world.on( - 'setWorker', - function(data){ - log_as_worker("Setting worker id: " + data); - worker_id = data; - // ipc.of.world.emit('app.framedata', {worker_id: worker_id, frame_data: 'sample_data_ignore'}); - } - ) + let ctx; + let canvas; + function prepareCanvas(size){ + canvas = createCanvas(...size); + ctx = canvas.getContext("2d"); + resize(); - ipc.of.world.on('app.framedataAck', function(data){ - // log_as_worker("Received ACK from server"); - }) + if(options.flashlight){ + for(const sizeRelative of FL_SIZES){ + const flCanvas = createCanvas(size[0] * 2, size[0] * 2); + const flCtx = flCanvas.getContext("2d"); - ipc.of.world.on('app.resumeWorking', function(data){ - // log_as_worker("Received ACK from server to resume work"); - WAITING_FOR_SERVER_ACK = false; - }) + flCtx.fillStyle = 'black'; + flCtx.fillRect(0, 0, flCanvas.width, flCanvas.height); - ipc.of.world.on('app.terminate', function(data){ - log_as_worker("Terminating..."); - process.exit(0); - }) + const flSize = sizeRelative * PLAYFIELD_HEIGHT * scale_multiplier / 2; - ipc.of.world.on( - 'receiveWork', - async function (data){ - // log_as_worker(data); - if (1) { - let { beatmap, start_time, end_time, time_frame, file_path, options, threads, current_frame, size } = data; - - function resize(){ - active_playfield_width = canvas.width * 0.7; - active_playfield_height = active_playfield_width * (3/4); - let position = playfieldPosition(0, 0); - let size = playfieldPosition(PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT); - scale_multiplier = (size[0] - position[0]) / PLAYFIELD_WIDTH; - } + const gradient = + flCtx.createRadialGradient( + flCanvas.width / 2, flCanvas.height / 2, + flSize * 0.9, + flCanvas.width / 2, flCanvas.height / 2, + flSize); - // Convert osu pixels position to canvas coordinates - function playfieldPosition(x, y){ - let ratio_x = x / PLAYFIELD_WIDTH; - let ratio_y = y / PLAYFIELD_HEIGHT; + gradient.addColorStop(0, 'rgba(0,0,0,1)'); + gradient.addColorStop(1, 'rgba(0,0,0,0)'); - return [ - active_playfield_width * ratio_x + canvas.width * 0.15, - active_playfield_height * ratio_y + canvas.height * 0.15 - ]; - } + flCtx.fillStyle = gradient; + flCtx.globalCompositeOperation = 'destination-out'; - const flImages = []; + flCtx.beginPath(); + flCtx.arc(flCanvas.width / 2, flCanvas.height / 2, flSize, 0, 2 * Math.PI); + flCtx.fill(); - let ctx; - let canvas; - function prepareCanvas(size){ - canvas = createCanvas(...size); - ctx = canvas.getContext("2d"); - resize(); + flImages.push(flCanvas); + } + } + } - if(options.flashlight){ - for(const sizeRelative of FL_SIZES){ - const flCanvas = createCanvas(size[0] * 2, size[0] * 2); - const flCtx = flCanvas.getContext("2d"); + function getCursorAtInterpolated(timestamp, replay){ + while(replay.lastCursor < replay.replay_data.length && replay.replay_data[replay.lastCursor].offset <= timestamp){ + replay.lastCursor++; + } + + let current = {...replay.replay_data[replay.lastCursor - 1]}; + let next = {...replay.replay_data[replay.lastCursor]} + + if(current === undefined || next === undefined){ + if(replay.replay_data.length > 0){ + return { + current: replay.replay_data[replay.replay_data.length - 1], + next: replay.replay_data[replay.replay_data.length - 1] + } + }else{ + return { + current: { + x: 0, + y: 0 + }, + next: { + x: 0, + y: 0 + } + } + } + } - flCtx.fillStyle = 'black'; - flCtx.fillRect(0, 0, flCanvas.width, flCanvas.height); + // Interpolate cursor position between two points for smooth motion - const flSize = sizeRelative * PLAYFIELD_HEIGHT * scale_multiplier / 2; + let current_start = current.offset; + let next_start = next.offset; - const gradient = - flCtx.createRadialGradient( - flCanvas.width / 2, flCanvas.height / 2, - flSize * 0.9, - flCanvas.width / 2, flCanvas.height / 2, - flSize); + let pos_current = [current.x, current.y]; + let pos_next = [next.x, next.y]; - gradient.addColorStop(0, 'rgba(0,0,0,1)'); - gradient.addColorStop(1, 'rgba(0,0,0,0)'); + timestamp -= current_start; + next_start -= current_start; - flCtx.fillStyle = gradient; - flCtx.globalCompositeOperation = 'destination-out'; + let progress = timestamp / next_start; - flCtx.beginPath(); - flCtx.arc(flCanvas.width / 2, flCanvas.height / 2, flSize, 0, 2 * Math.PI); - flCtx.fill(); + let distance = vectorDistance(pos_current, pos_next); - flImages.push(flCanvas); - } - } - } + let n = Math.max(1, progress * distance); - function getCursorAtInterpolated(timestamp, replay){ - while(replay.lastCursor < replay.replay_data.length && replay.replay_data[replay.lastCursor].offset <= timestamp){ - replay.lastCursor++; - } + if(distance > 0){ + current.x = pos_current[0] + (n / distance) * (pos_next[0] - pos_current[0]); + current.y = pos_current[1] + (n / distance) * (pos_next[1] - pos_current[1]); + } - let current = {...replay.replay_data[replay.lastCursor - 1]}; - let next = {...replay.replay_data[replay.lastCursor]} - - if(current === undefined || next === undefined){ - if(replay.replay_data.length > 0){ - return { - current: replay.replay_data[replay.replay_data.length - 1], - next: replay.replay_data[replay.replay_data.length - 1] - } - }else{ - return { - current: { - x: 0, - y: 0 - }, - next: { - x: 0, - y: 0 - } - } - } - } + return {current: current, next: next}; + } - // Interpolate cursor position between two points for smooth motion + function interpolateReplayData(replay){ + const interpolatedReplay = {lastCursor: 0, replay_data: []}; - let current_start = current.offset; - let next_start = next.offset; + const frametime = 4; - let pos_current = [current.x, current.y]; - let pos_next = [next.x, next.y]; + for(let timestamp = 0; timestamp < end_time; timestamp += frametime){ + const replayPoint = getCursorAtInterpolated(timestamp, replay).current; + replayPoint.offset = timestamp; + interpolatedReplay.replay_data.push(replayPoint); + } - timestamp -= current_start; - next_start -= current_start; + return interpolatedReplay; + } - let progress = timestamp / next_start; + function getScoringFrames(timestamp, scoringFrames){ + const output = []; - let distance = vectorDistance(pos_current, pos_next); + let i = scoringFrames.findIndex(a => a.offset > timestamp - 2000); - let n = Math.max(1, progress * distance); + while(scoringFrames[i].offset <= timestamp){ + output.push(scoringFrames[i]); + i++; + } - if(distance > 0){ - current.x = pos_current[0] + (n / distance) * (pos_next[0] - pos_current[0]); - current.y = pos_current[1] + (n / distance) * (pos_next[1] - pos_current[1]); - } + return output; + } - return {current: current, next: next}; + function getCursorAt(timestamp, replay){ + while(replay.lastCursor < replay.replay_data.length && replay.replay_data[replay.lastCursor].offset <= timestamp){ + replay.lastCursor++; + } + + let current = replay.replay_data[replay.lastCursor - 1]; + let next = replay.replay_data[replay.lastCursor]; + let previous = []; + + for(let i = 0; i < Math.min(replay.lastCursor - 2, 20); i++) + previous.push(replay.replay_data[replay.lastCursor - i]); + + if(current === undefined || next === undefined){ + if(replay.replay_data.length > 0){ + return { + current: replay.replay_data[replay.replay_data.length - 1], + next: replay.replay_data[replay.replay_data.length - 1] + } + }else{ + return { + current: { + x: 0, + y: 0 + }, + next: { + x: 0, + y: 0 } + } + } + } - function interpolateReplayData(replay){ - const interpolatedReplay = {lastCursor: 0, replay_data: []}; - - const frametime = 4; + return {previous, current, next}; + } - for(let timestamp = 0; timestamp < end_time; timestamp += frametime){ - const replayPoint = getCursorAtInterpolated(timestamp, replay).current; - replayPoint.offset = timestamp; - interpolatedReplay.replay_data.push(replayPoint); - } + function vectorDistance(hitObject1, hitObject2){ + return Math.sqrt((hitObject2[0] - hitObject1[0]) * (hitObject2[0] - hitObject1[0]) + + (hitObject2[1] - hitObject1[1]) * (hitObject2[1] - hitObject1[1])); + } - return interpolatedReplay; - } + function processFrame(time, options){ + let hitObjectsOnScreen = []; - function getScoringFrames(timestamp, scoringFrames){ - const output = []; + ctx.globalAlpha = 1; - let i = scoringFrames.findIndex(a => a.offset > timestamp - 2000); + // Generate array with all hit objects currently visible + beatmap.hitObjects.forEach(hitObject => { + if(time >= hitObject.startTime - beatmap.TimePreempt && hitObject.endTime - time > -200) + hitObjectsOnScreen.push(hitObject); + }); - while(scoringFrames[i].offset <= timestamp){ - output.push(scoringFrames[i]); - i++; - } + ctx.clearRect(0, 0, canvas.width, canvas.height); - return output; - } + if(options.black){ + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } - function getCursorAt(timestamp, replay){ - while(replay.lastCursor < replay.replay_data.length && replay.replay_data[replay.lastCursor].offset <= timestamp){ - replay.lastCursor++; - } + hitObjectsOnScreen.sort(function(a, b){ return a.startTime - b.startTime; }); - let current = replay.replay_data[replay.lastCursor - 1]; - let next = replay.replay_data[replay.lastCursor]; - let previous = []; - - for(let i = 0; i < Math.min(replay.lastCursor - 2, 20); i++) - previous.push(replay.replay_data[replay.lastCursor - i]); - - if(current === undefined || next === undefined){ - if(replay.replay_data.length > 0){ - return { - current: replay.replay_data[replay.replay_data.length - 1], - next: replay.replay_data[replay.replay_data.length - 1] - } - }else{ - return { - current: { - x: 0, - y: 0 - }, - next: { - x: 0, - y: 0 - } - } - } - } + ctx.strokeStyle = 'rgba(255,255,255,0.3)'; + ctx.lineWidth = 3 * scale_multiplier; + ctx.shadowColor = 'transparent'; - return {previous, current, next}; - } + // Draw follow points + hitObjectsOnScreen.forEach(function(hitObject, index){ + if(index < hitObjectsOnScreen.length - 1){ + let nextObject = hitObjectsOnScreen[index + 1]; - function vectorDistance(hitObject1, hitObject2){ - return Math.sqrt((hitObject2[0] - hitObject1[0]) * (hitObject2[0] - hitObject1[0]) - + (hitObject2[1] - hitObject1[1]) * (hitObject2[1] - hitObject1[1])); - } + if(isNaN(hitObject.endPosition) && isNaN(nextObject.position)) + return false; - function processFrame(time, options){ - let hitObjectsOnScreen = []; + let distance = vectorDistance(hitObject.endPosition, nextObject.position); - ctx.globalAlpha = 1; + if(time >= (nextObject.startTime - beatmap.TimePreempt) && time < (nextObject.startTime + beatmap.HitWindow50) && distance > 80){ + let start_position = playfieldPosition(...hitObject.endPosition); + let end_position = playfieldPosition(...nextObject.position); - // Generate array with all hit objects currently visible - beatmap.hitObjects.forEach(hitObject => { - if(time >= hitObject.startTime - beatmap.TimePreempt && hitObject.endTime - time > -200) - hitObjectsOnScreen.push(hitObject); - }); + let progress_0 = nextObject.startTime - beatmap.TimePreempt - ctx.clearRect(0, 0, canvas.width, canvas.height); + let a = progress_0; - if(options.black){ - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } + progress_0 += time - progress_0; + let progress_1 = nextObject.startTime - beatmap.TimeFadein; - hitObjectsOnScreen.sort(function(a, b){ return a.startTime - b.startTime; }); + progress_1 -= a + progress_0 -= a; - ctx.strokeStyle = 'rgba(255,255,255,0.3)'; - ctx.lineWidth = 3 * scale_multiplier; - ctx.shadowColor = 'transparent'; + let progress = Math.min(1, progress_0 / progress_1 * 2); - // Draw follow points - hitObjectsOnScreen.forEach(function(hitObject, index){ - if(index < hitObjectsOnScreen.length - 1){ - let nextObject = hitObjectsOnScreen[index + 1]; + let v = vectorSubtract(end_position, start_position); - if(isNaN(hitObject.endPosition) && isNaN(nextObject.position)) - return false; + v[0] *= progress; + v[1] *= progress; - let distance = vectorDistance(hitObject.endPosition, nextObject.position); - if(time >= (nextObject.startTime - beatmap.TimePreempt) && time < (nextObject.startTime + beatmap.HitWindow50) && distance > 80){ - let start_position = playfieldPosition(...hitObject.endPosition); - let end_position = playfieldPosition(...nextObject.position); + ctx.beginPath(); + ctx.moveTo(...start_position); + ctx.lineTo(vectorAdd(start_position, v[0])); + ctx.stroke(); - let progress_0 = nextObject.startTime - beatmap.TimePreempt + //then shift x by cos(angle)*radius and y by sin(angle)*radius (TODO) + } + } + }); - let a = progress_0; + // Sort hit objects from latest to earliest so the earliest hit object is at the front + hitObjectsOnScreen.reverse(); - progress_0 += time - progress_0; - let progress_1 = nextObject.startTime - beatmap.TimeFadein; + hitObjectsOnScreen.forEach(function(hitObject, index){ + // Check if hit object could be visible at current timestamp + if(time < hitObject.startTime || hitObject.objectName !== "circle" && time < hitObject.endTime + 200){ + // Apply approach rate + let opacity = (time - (hitObject.startTime - beatmap.TimePreempt)) / (beatmap.TimePreempt - beatmap.TimeFadein); - progress_1 -= a - progress_0 -= a; + if(hitObject.objectName !== 'circle') + opacity = 1 - (time - hitObject.endTime) / 200; - let progress = Math.min(1, progress_0 / progress_1 * 2); + // Calculate relative approach circle size (number from 0 to 1) + let approachCircle = 1 - (time - (hitObject.startTime - beatmap.TimeFadein)) / beatmap.TimeFadein; - let v = vectorSubtract(end_position, start_position); + if(approachCircle < 0) approachCircle = 0; + if(opacity > 1) opacity = 1; - v[0] *= progress; - v[1] *= progress; + ctx.shadowBlur = 4 * scale_multiplier; + ctx.fillStyle = "rgba(40,40,40,0.2)"; + let followpoint_index; + let followpoint_progress = 0; - ctx.beginPath(); - ctx.moveTo(...start_position); - ctx.lineTo(vectorAdd(start_position, v[0])); - ctx.stroke(); + // Draw slider + if(hitObject.objectName === "slider"){ + let sliderOpacity = opacity; - //then shift x by cos(angle)*radius and y by sin(angle)*radius (TODO) - } - } - }); + if(options.hidden){ + const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; - // Sort hit objects from latest to earliest so the earliest hit object is at the front - hitObjectsOnScreen.reverse(); + if(time >= fadeOutStartTime) + sliderOpacity = 1 - (time - fadeOutStartTime) / (hitObject.endTime - fadeOutStartTime); - hitObjectsOnScreen.forEach(function(hitObject, index){ - // Check if hit object could be visible at current timestamp - if(time < hitObject.startTime || hitObject.objectName !== "circle" && time < hitObject.endTime + 200){ - // Apply approach rate - let opacity = (time - (hitObject.startTime - beatmap.TimePreempt)) / (beatmap.TimePreempt - beatmap.TimeFadein); + if(sliderOpacity < 0) + sliderOpacity = 0; + } - if(hitObject.objectName !== 'circle') - opacity = 1 - (time - hitObject.endTime) / 200; + ctx.globalAlpha = sliderOpacity; - // Calculate relative approach circle size (number from 0 to 1) - let approachCircle = 1 - (time - (hitObject.startTime - beatmap.TimeFadein)) / beatmap.TimeFadein; + ctx.lineWidth = 5 * scale_multiplier; + ctx.strokeStyle = "rgba(255,255,255,0.7)"; - if(approachCircle < 0) approachCircle = 0; - if(opacity > 1) opacity = 1; + let snakingStart = hitObject.startTime - beatmap.TimePreempt; + let snakingFinish = hitObject.startTime - beatmap.TimeFadein; - ctx.shadowBlur = 4 * scale_multiplier; - ctx.fillStyle = "rgba(40,40,40,0.2)"; + let snakingProgress = Math.max(0, Math.min(1, (time - snakingStart) / (snakingFinish - snakingStart))); - let followpoint_index; - let followpoint_progress = 0; + let render_dots = []; - // Draw slider - if(hitObject.objectName === "slider"){ - let sliderOpacity = opacity; + for(let x = 0; x < Math.floor(hitObject.SliderDots.length * snakingProgress); x++) + render_dots.push(hitObject.SliderDots[x]); - if(options.hidden){ - const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; + // Use stroke with rounded ends to "fake" a slider path + ctx.beginPath(); + ctx.lineCap = "round"; + ctx.strokeStyle = 'rgba(255,255,255,0.7)'; + ctx.shadowColor = 'transparent'; + ctx.lineJoin = "round" - if(time >= fadeOutStartTime) - sliderOpacity = 1 - (time - fadeOutStartTime) / (hitObject.endTime - fadeOutStartTime); + ctx.lineWidth = scale_multiplier * beatmap.Radius * 2 - if(sliderOpacity < 0) - sliderOpacity = 0; - } + // Draw a path through all slider dots generated earlier + for(let x = 0; x < render_dots.length; x++){ + let dot = render_dots[x]; + let position = playfieldPosition(...dot); - ctx.globalAlpha = sliderOpacity; + if(x === 0){ + ctx.moveTo(...position); + }else{ + ctx.lineTo(...position); + } + } - ctx.lineWidth = 5 * scale_multiplier; - ctx.strokeStyle = "rgba(255,255,255,0.7)"; + ctx.stroke(); - let snakingStart = hitObject.startTime - beatmap.TimePreempt; - let snakingFinish = hitObject.startTime - beatmap.TimeFadein; + ctx.lineWidth = scale_multiplier * (beatmap.Radius * 2 - 10); + ctx.strokeStyle = 'rgba(0,0,0,0.6)'; - let snakingProgress = Math.max(0, Math.min(1, (time - snakingStart) / (snakingFinish - snakingStart))); + ctx.stroke(); - let render_dots = []; + let currentTurn = 0, currentOffset, currentTurnStart; - for(let x = 0; x < Math.floor(hitObject.SliderDots.length * snakingProgress); x++) - render_dots.push(hitObject.SliderDots[x]); + // Get slider dot corresponding to the current follow point position + if(time >= hitObject.startTime && time <= hitObject.endTime){ + currentTurn = Math.floor((time - hitObject.startTime) / (hitObject.duration / hitObject.repeatCount)); + currentTurnStart = hitObject.startTime + hitObject.duration / hitObject.repeatCount * currentTurn; + currentOffset = (time - hitObject.startTime) / (hitObject.duration / hitObject.repeatCount) - currentTurn; - // Use stroke with rounded ends to "fake" a slider path - ctx.beginPath(); - ctx.lineCap = "round"; - ctx.strokeStyle = 'rgba(255,255,255,0.7)'; - ctx.shadowColor = 'transparent'; - ctx.lineJoin = "round" + let dot_index = 0; - ctx.lineWidth = scale_multiplier * beatmap.Radius * 2 + if(currentTurn % 2 === 0) + dot_index = currentOffset * hitObject.SliderDots.length; + else + dot_index = (1 - currentOffset) * hitObject.SliderDots.length; - // Draw a path through all slider dots generated earlier - for(let x = 0; x < render_dots.length; x++){ - let dot = render_dots[x]; - let position = playfieldPosition(...dot); + followpoint_index = Math.floor(dot_index); - if(x === 0){ - ctx.moveTo(...position); - }else{ - ctx.lineTo(...position); - } - } + /* Progress number from 0 to 1 to check how much relative distance to the next slider dot is left, + used in interpolation later to always have smooth follow points */ + followpoint_progress = dot_index - followpoint_index; + }else{ + if(time < hitObject.startTime){ + currentOffset = 0; + currentTurnStart = hitObject.startTime - beatmap.TimePreempt; + }else{ + currentOffset = 1; + currentTurnStart = hitObject.endTime; + } + } - ctx.stroke(); + // Render slider ticks (WIP) + if(time <= hitObject.endTime){ + ctx.strokeStyle = 'rgba(255,255,255,0.8)'; + ctx.lineWidth = 5 * scale_multiplier; - ctx.lineWidth = scale_multiplier * (beatmap.Radius * 2 - 10); - ctx.strokeStyle = 'rgba(0,0,0,0.6)'; + let slider_ticks = hitObject.SliderTicks.slice(); - ctx.stroke(); + // Reverse slider ticks depending on current slider direction + if(currentTurn > 0 && currentTurn % 2 !== 0) + slider_ticks.reverse(); - let currentTurn = 0, currentOffset, currentTurnStart; + let max = Math.floor(slider_ticks.length * snakingProgress); - // Get slider dot corresponding to the current follow point position - if(time >= hitObject.startTime && time <= hitObject.endTime){ - currentTurn = Math.floor((time - hitObject.startTime) / (hitObject.duration / hitObject.repeatCount)); - currentTurnStart = hitObject.startTime + hitObject.duration / hitObject.repeatCount * currentTurn; - currentOffset = (time - hitObject.startTime) / (hitObject.duration / hitObject.repeatCount) - currentTurn; + let offset = time - currentTurnStart; - let dot_index = 0; + for(let x = 0; x < max; x++){ + if(currentTurn > 0) + ctx.globalAlpha = Math.min(1, Math.max(0, Math.min(1, (time - x * 40 - currentTurnStart) / 50))); + else if(time < hitObject.startTime) + ctx.globalAlpha = Math.min(1, Math.max(0, Math.min(1, (time - (max - x) * 40 - currentTurnStart) / 50))); - if(currentTurn % 2 === 0) - dot_index = currentOffset * hitObject.SliderDots.length; - else - dot_index = (1 - currentOffset) * hitObject.SliderDots.length; + let tick = slider_ticks[x]; - followpoint_index = Math.floor(dot_index); + if(currentTurn > 0 && currentTurn % 2 !== 0) + tick.offset = tick.reverseOffset; - /* Progress number from 0 to 1 to check how much relative distance to the next slider dot is left, - used in interpolation later to always have smooth follow points */ - followpoint_progress = dot_index - followpoint_index; - }else{ - if(time < hitObject.startTime){ - currentOffset = 0; - currentTurnStart = hitObject.startTime - beatmap.TimePreempt; - }else{ - currentOffset = 1; - currentTurnStart = hitObject.endTime; - } - } + if(followpoint_index && tick.offset < offset) + continue; - // Render slider ticks (WIP) - if(time <= hitObject.endTime){ - ctx.strokeStyle = 'rgba(255,255,255,0.8)'; - ctx.lineWidth = 5 * scale_multiplier; + let position = playfieldPosition(...tick.position); + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * beatmap.Radius / 5, 0, 2 * Math.PI, false); + ctx.stroke(); + } - let slider_ticks = hitObject.SliderTicks.slice(); + ctx.globalAlpha = opacity; + } - // Reverse slider ticks depending on current slider direction - if(currentTurn > 0 && currentTurn % 2 !== 0) - slider_ticks.reverse(); + // Render repeat arrow + for(let x = 1; x < hitObject.repeatCount; x++){ + let repeatOffset = hitObject.startTime + x * (hitObject.duration / hitObject.repeatCount); + let fadeInStart = x === 1 ? snakingFinish : repeatOffset - (hitObject.duration / hitObject.repeatCount) * 2; + let repeatPosition = (x - 1) % 2 === 0 ? hitObject.endPosition : hitObject.position; - let max = Math.floor(slider_ticks.length * snakingProgress); + let timeSince = Math.max(0, Math.min(1, (time - repeatOffset) / 200)); - let offset = time - currentTurnStart; + if(time >= repeatOffset) + ctx.globalAlpha = (1 - timeSince); + else + ctx.globalAlpha = Math.min(1, Math.max(0, (time - fadeInStart) / 50)); - for(let x = 0; x < max; x++){ - if(currentTurn > 0) - ctx.globalAlpha = Math.min(1, Math.max(0, Math.min(1, (time - x * 40 - currentTurnStart) / 50))); - else if(time < hitObject.startTime) - ctx.globalAlpha = Math.min(1, Math.max(0, Math.min(1, (time - (max - x) * 40 - currentTurnStart) / 50))); + let sizeFactor = 1 + timeSince * 0.3; - let tick = slider_ticks[x]; + let comparePosition = + (x - 1) % 2 === 0 ? hitObject.SliderDots[hitObject.SliderDots.length - 2] : hitObject.SliderDots[1]; - if(currentTurn > 0 && currentTurn % 2 !== 0) - tick.offset = tick.reverseOffset; + let repeatDirection = Math.atan2(comparePosition[1] - repeatPosition[1], comparePosition[0] - repeatPosition[0]); - if(followpoint_index && tick.offset < offset) - continue; + let position = playfieldPosition(...repeatPosition); - let position = playfieldPosition(...tick.position); - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * beatmap.Radius / 5, 0, 2 * Math.PI, false); - ctx.stroke(); - } + let size = beatmap.Radius * 2 * scale_multiplier; - ctx.globalAlpha = opacity; - } + ctx.save(); - // Render repeat arrow - for(let x = 1; x < hitObject.repeatCount; x++){ - let repeatOffset = hitObject.startTime + x * (hitObject.duration / hitObject.repeatCount); - let fadeInStart = x === 1 ? snakingFinish : repeatOffset - (hitObject.duration / hitObject.repeatCount) * 2; - let repeatPosition = (x - 1) % 2 === 0 ? hitObject.endPosition : hitObject.position; + ctx.translate(...position); + ctx.rotate(repeatDirection); - let timeSince = Math.max(0, Math.min(1, (time - repeatOffset) / 200)); + position = [0, 0]; - if(time >= repeatOffset) - ctx.globalAlpha = (1 - timeSince); - else - ctx.globalAlpha = Math.min(1, Math.max(0, (time - fadeInStart) / 50)); + ctx.lineWidth = 5 * scale_multiplier; + ctx.beginPath(); + ctx.strokeStyle = "rgba(255,255,255,0.85)"; - let sizeFactor = 1 + timeSince * 0.3; + if(!options.noshadow) + ctx.shadowColor = "rgba(0,0,0,0.7)"; - let comparePosition = - (x - 1) % 2 === 0 ? hitObject.SliderDots[hitObject.SliderDots.length - 2] : hitObject.SliderDots[1]; + // Fill circle with combo color instead of leaving see-through circles + if(options.fill){ + ctx.beginPath(); + ctx.fillStyle = hitObject.Color; + ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); + ctx.fill(); + } - let repeatDirection = Math.atan2(comparePosition[1] - repeatPosition[1], comparePosition[0] - repeatPosition[0]); + // Draw circle border + ctx.beginPath(); + ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.stroke(); - let position = playfieldPosition(...repeatPosition); + ctx.fillStyle = 'white'; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; - let size = beatmap.Radius * 2 * scale_multiplier; + let fontSize = 18; + fontSize += 16 * (1 - (beatmap.CircleSize / 10)); - ctx.save(); + fontSize *= scale_multiplier * sizeFactor; - ctx.translate(...position); - ctx.rotate(repeatDirection); + // Draw combo number on circle + ctx.font = `${fontSize}px sans-serif`; - position = [0, 0]; + ctx.fillText("➤", ...position); - ctx.lineWidth = 5 * scale_multiplier; - ctx.beginPath(); - ctx.strokeStyle = "rgba(255,255,255,0.85)"; + /* this doesn't render correctly for some reason??? + using text for now I guess (TODO: FIX) */ + //ctx.drawImage(images.arrow, ...position, size, size); - if(!options.noshadow) - ctx.shadowColor = "rgba(0,0,0,0.7)"; + ctx.restore(); + } - // Fill circle with combo color instead of leaving see-through circles - if(options.fill){ - ctx.beginPath(); - ctx.fillStyle = hitObject.Color; - ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); - ctx.fill(); - } + ctx.globalAlpha = opacity; + } - // Draw circle border - ctx.beginPath(); - ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.stroke(); + let circleOpacity = opacity; - ctx.fillStyle = 'white'; - ctx.textBaseline = "middle"; - ctx.textAlign = "center"; + if(options.hidden && circleOpacity >= 1){ + const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; - let fontSize = 18; - fontSize += 16 * (1 - (beatmap.CircleSize / 10)); + if(time >= fadeOutStartTime) + circleOpacity = 1 - (time - fadeOutStartTime) / (beatmap.TimePreempt * 0.3); - fontSize *= scale_multiplier * sizeFactor; + if(circleOpacity < 0) + circleOpacity = 0; + } - // Draw combo number on circle - ctx.font = `${fontSize}px sans-serif`; + ctx.globalAlpha = circleOpacity; - ctx.fillText("➤", ...position); + // Draw circles or slider heads + if(hitObject.objectName !== "spinner"){ + ctx.lineWidth = 5 * scale_multiplier; + ctx.beginPath(); + ctx.strokeStyle = "rgba(255,255,255,0.85)"; - /* this doesn't render correctly for some reason??? - using text for now I guess (TODO: FIX) */ - //ctx.drawImage(images.arrow, ...position, size, size); + if(time < hitObject.startTime){ + if(!options.noshadow) + ctx.shadowColor = "rgba(0,0,0,0.7)"; - ctx.restore(); - } + let position = playfieldPosition(...hitObject.position); - ctx.globalAlpha = opacity; - } + // Fill circle with combo color instead of leaving see-through circles + if(options.fill){ + ctx.beginPath(); + ctx.fillStyle = hitObject.Color; + ctx.arc(...position, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); + ctx.fill(); + } - let circleOpacity = opacity; + // Draw circle border + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.stroke(); - if(options.hidden && circleOpacity >= 1){ - const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; + ctx.fillStyle = 'white'; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; - if(time >= fadeOutStartTime) - circleOpacity = 1 - (time - fadeOutStartTime) / (beatmap.TimePreempt * 0.3); + let fontSize = 16; + fontSize += 16 * (1 - (beatmap.CircleSize / 10)); - if(circleOpacity < 0) - circleOpacity = 0; - } + fontSize *= scale_multiplier; - ctx.globalAlpha = circleOpacity; + // Draw combo number on circle + ctx.font = `${fontSize}px sans-serif`; + ctx.fillText(hitObject.ComboNumber, position[0], position[1]); - // Draw circles or slider heads - if(hitObject.objectName !== "spinner"){ - ctx.lineWidth = 5 * scale_multiplier; - ctx.beginPath(); - ctx.strokeStyle = "rgba(255,255,255,0.85)"; + // Draw approach circle + if(approachCircle > 0 && !options.hidden){ + ctx.strokeStyle = 'white'; + ctx.lineWidth = 2 * scale_multiplier; + ctx.beginPath(); + let position = playfieldPosition(...hitObject.position); + ctx.arc(...position, scale_multiplier * (beatmap.Radius + approachCircle * (beatmap.Radius * 2)), 0, 2 * Math.PI, false); + ctx.stroke(); + } + } - if(time < hitObject.startTime){ - if(!options.noshadow) - ctx.shadowColor = "rgba(0,0,0,0.7)"; + // Draw follow point if there's currently one visible + if(followpoint_index + && Array.isArray(hitObject.SliderDots[followpoint_index]) + && hitObject.SliderDots[followpoint_index].length === 2 + ){ + let pos_current = hitObject.SliderDots[followpoint_index]; - let position = playfieldPosition(...hitObject.position); + if(hitObject.SliderDots.length - 1 > followpoint_index){ + // Interpolate follow point position - // Fill circle with combo color instead of leaving see-through circles - if(options.fill){ - ctx.beginPath(); - ctx.fillStyle = hitObject.Color; - ctx.arc(...position, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); - ctx.fill(); - } + let pos_next = hitObject.SliderDots[followpoint_index + 1]; - // Draw circle border - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.stroke(); + let distance = vectorDistance(pos_current, pos_next); - ctx.fillStyle = 'white'; - ctx.textBaseline = "middle"; - ctx.textAlign = "center"; + let n = Math.max(1, followpoint_progress * distance); - let fontSize = 16; - fontSize += 16 * (1 - (beatmap.CircleSize / 10)); + if(distance > 0){ + pos_current = [ + pos_current[0] + (n / distance) * (pos_next[0] - pos_current[0]), + pos_current[1] + (n / distance) * (pos_next[1] - pos_current[1]) + ] + } + } - fontSize *= scale_multiplier; + ctx.globalAlpha = 1; - // Draw combo number on circle - ctx.font = `${fontSize}px sans-serif`; - ctx.fillText(hitObject.ComboNumber, position[0], position[1]); + let position; - // Draw approach circle - if(approachCircle > 0 && !options.hidden){ - ctx.strokeStyle = 'white'; - ctx.lineWidth = 2 * scale_multiplier; - ctx.beginPath(); - let position = playfieldPosition(...hitObject.position); - ctx.arc(...position, scale_multiplier * (beatmap.Radius + approachCircle * (beatmap.Radius * 2)), 0, 2 * Math.PI, false); - ctx.stroke(); - } - } + // Draw follow point in circle - // Draw follow point if there's currently one visible - if(followpoint_index - && Array.isArray(hitObject.SliderDots[followpoint_index]) - && hitObject.SliderDots[followpoint_index].length === 2 - ){ - let pos_current = hitObject.SliderDots[followpoint_index]; + ctx.fillStyle = "rgba(255,255,255,0.3)"; + ctx.beginPath(); - if(hitObject.SliderDots.length - 1 > followpoint_index){ - // Interpolate follow point position + position = playfieldPosition(...pos_current); + ctx.arc(...position, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); + ctx.fill(); - let pos_next = hitObject.SliderDots[followpoint_index + 1]; + // Draw follow circle visible around the follow point - let distance = vectorDistance(pos_current, pos_next); + ctx.fillStyle = "rgba(255,255,255,0.8)"; + ctx.beginPath(); - let n = Math.max(1, followpoint_progress * distance); + position = playfieldPosition(...pos_current); + ctx.arc(...position, scale_multiplier * (beatmap.FollowpointRadius), 0, 2 * Math.PI, false); + ctx.stroke(); + } - if(distance > 0){ - pos_current = [ - pos_current[0] + (n / distance) * (pos_next[0] - pos_current[0]), - pos_current[1] + (n / distance) * (pos_next[1] - pos_current[1]) - ] - } - } + }else{ + // Draw spinner + ctx.strokeStyle = "white"; + ctx.globalAlpha = opacity; + ctx.lineWidth = 10 * scale_multiplier; - ctx.globalAlpha = 1; + let position = playfieldPosition(PLAYFIELD_WIDTH / 2, PLAYFIELD_HEIGHT / 2); - let position; + // Rotate spinner (WIP) + /* + if(beatmap.Replay && time >= hitObject.startTime){ + let replay_point = getCursorAt(time, beatmap.Replay); - // Draw follow point in circle + if(replay_point){ + let { current } = replay_point; - ctx.fillStyle = "rgba(255,255,255,0.3)"; - ctx.beginPath(); + let radians = Math.atan2(current.y - PLAYFIELD_WIDTH / 2, current.x - PLAYFIELD_HEIGHT / 2); - position = playfieldPosition(...pos_current); - ctx.arc(...position, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); - ctx.fill(); + position = [ + position[0] + 2.5 * Math.cos(radians), + position[1] + 2.5 * Math.sin(radians) + ]; + } + } + */ + + // Outer spinner circle + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * 240, 0, 2 * Math.PI, false); + ctx.stroke(); + + // Inner spinner circle + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * 30, 0, 2 * Math.PI, false); + ctx.stroke(); + } + } - // Draw follow circle visible around the follow point + if(!options.hidden && time >= hitObject.startTime && hitObject.startTime - time > -200){ + // Draw fading out circles + if(hitObject.objectName !== "spinner"){ + // Increase circle size the further it's faded out + let hitOffset = 0; + + if(beatmap.Replay.auto !== true){ + if(hitObject.hitOffset == null) + hitOffset += beatmap.HitWindow50; + else + hitOffset += hitObject.hitOffset; + } - ctx.fillStyle = "rgba(255,255,255,0.8)"; - ctx.beginPath(); + let timeSince = Math.min(1, Math.max(0, (time - (hitObject.startTime + hitOffset)) / 200)); + let opacity = 1 - timeSince; + let sizeFactor = 1 + timeSince * 0.3; - position = playfieldPosition(...pos_current); - ctx.arc(...position, scale_multiplier * (beatmap.FollowpointRadius), 0, 2 * Math.PI, false); - ctx.stroke(); - } - - }else{ - // Draw spinner - ctx.strokeStyle = "white"; - ctx.globalAlpha = opacity; - ctx.lineWidth = 10 * scale_multiplier; - - let position = playfieldPosition(PLAYFIELD_WIDTH / 2, PLAYFIELD_HEIGHT / 2); - - // Rotate spinner (WIP) - /* - if(beatmap.Replay && time >= hitObject.startTime){ - let replay_point = getCursorAt(time, beatmap.Replay); - - if(replay_point){ - let { current } = replay_point; - - let radians = Math.atan2(current.y - PLAYFIELD_WIDTH / 2, current.x - PLAYFIELD_HEIGHT / 2); - - position = [ - position[0] + 2.5 * Math.cos(radians), - position[1] + 2.5 * Math.sin(radians) - ]; - } - } - */ + ctx.globalAlpha = opacity; - // Outer spinner circle - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * 240, 0, 2 * Math.PI, false); - ctx.stroke(); + if(!options.noshadow) + ctx.shadowColor = "rgba(0,0,0,0.7)"; - // Inner spinner circle - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * 30, 0, 2 * Math.PI, false); - ctx.stroke(); - } - } + ctx.lineWidth = 6 * scale_multiplier; + ctx.beginPath(); + ctx.strokeStyle = "rgba(255,255,255,0.85)"; - if(!options.hidden && time >= hitObject.startTime && hitObject.startTime - time > -200){ - // Draw fading out circles - if(hitObject.objectName !== "spinner"){ - // Increase circle size the further it's faded out - let hitOffset = 0; + let position = playfieldPosition(...hitObject.position); - if(beatmap.Replay.auto !== true){ - if(hitObject.hitOffset == null) - hitOffset += beatmap.HitWindow50; - else - hitOffset += hitObject.hitOffset; - } + if(options.fill){ + ctx.beginPath(); + ctx.fillStyle = hitObject.Color; + ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); + ctx.fill(); + } - let timeSince = Math.min(1, Math.max(0, (time - (hitObject.startTime + hitOffset)) / 200)); - let opacity = 1 - timeSince; - let sizeFactor = 1 + timeSince * 0.3; + ctx.beginPath(); + ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.stroke(); - ctx.globalAlpha = opacity; + ctx.fillStyle = 'white'; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; - if(!options.noshadow) - ctx.shadowColor = "rgba(0,0,0,0.7)"; + let fontSize = 16; + fontSize += 16 * (1 - (beatmap.CircleSize / 10)); - ctx.lineWidth = 6 * scale_multiplier; - ctx.beginPath(); - ctx.strokeStyle = "rgba(255,255,255,0.85)"; + fontSize *= scale_multiplier * sizeFactor; - let position = playfieldPosition(...hitObject.position); + ctx.font = `${fontSize}px sans-serif`; + ctx.fillText(hitObject.ComboNumber, ...position); + } + } + }); - if(options.fill){ - ctx.beginPath(); - ctx.fillStyle = hitObject.Color; - ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); - ctx.fill(); - } + if(options.analyze){ + for(const hitObject of beatmap.hitObjects){ + if(hitObject.objectName === 'spinner') + continue; - ctx.beginPath(); - ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.stroke(); + if(hitObject.hitResult > 0 && hitObject.objectName === 'circle' + || hitObject.MissedSliderStart < 1 && hitObject.objectName === 'slider') + continue; - ctx.fillStyle = 'white'; - ctx.textBaseline = "middle"; - ctx.textAlign = "center"; + if(time < hitObject.startTime) + continue; - let fontSize = 16; - fontSize += 16 * (1 - (beatmap.CircleSize / 10)); + if(time - hitObject.startTime > 750) + continue; - fontSize *= scale_multiplier * sizeFactor; + const position = playfieldPosition(...hitObject.position); - ctx.font = `${fontSize}px sans-serif`; - ctx.fillText(hitObject.ComboNumber, ...position); - } - } - }); + ctx.globalAlpha = 1; + ctx.lineWidth = 3 * scale_multiplier; + ctx.strokeStyle = options.fill ? '#fa2f2f' : 'white'; + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.stroke(); + } + } - if(options.analyze){ - for(const hitObject of beatmap.hitObjects){ - if(hitObject.objectName === 'spinner') - continue; + if(beatmap.ScoringFrames && beatmap.Replay.auto !== true){ + //const scoringFrames = getScoringFrames(time, beatmap.ScoringFrames); - if(hitObject.hitResult > 0 && hitObject.objectName === 'circle' - || hitObject.MissedSliderStart < 1 && hitObject.objectName === 'slider') - continue; + let previousFramesIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time - 5000); - if(time < hitObject.startTime) - continue; + let currentFrameIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time) - 1; - if(time - hitObject.startTime > 750) - continue; + let currentFrame = beatmap.ScoringFrames[currentFrameIndex]; - const position = playfieldPosition(...hitObject.position); + if(currentFrame == null) + currentFrame = beatmap.ScoringFrames[beatmap.ScoringFrames.length - 1]; - ctx.globalAlpha = 1; - ctx.lineWidth = 3 * scale_multiplier; - ctx.strokeStyle = options.fill ? '#fa2f2f' : 'white'; - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.stroke(); - } - } + const scoringFrames = []; - if(beatmap.ScoringFrames && beatmap.Replay.auto !== true){ - //const scoringFrames = getScoringFrames(time, beatmap.ScoringFrames); + if(options.flashlight){ + ctx.globalAlpha = 1; - let previousFramesIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time - 5000); + let { current } = getCursorAt(time, beatmap.ReplayInterpolated); - let currentFrameIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time) - 1; + const { combo } = currentFrame; - let currentFrame = beatmap.ScoringFrames[currentFrameIndex]; + let flIndex = 0; - if(currentFrame == null) - currentFrame = beatmap.ScoringFrames[beatmap.ScoringFrames.length - 1]; + if(combo >= 100) + flIndex = 1; + else if(combo >= 200) + flIndex = 2; - const scoringFrames = []; + const flImage = flImages[flIndex]; - if(options.flashlight){ - ctx.globalAlpha = 1; + const cursorPos = playfieldPosition(current.x, current.y); - let { current } = getCursorAt(time, beatmap.ReplayInterpolated); + ctx.drawImage(flImage, cursorPos[0] - flImage.width / 2, cursorPos[1] - flImage.height / 2); - const { combo } = currentFrame; + const currentSlider = beatmap.hitObjects.find(a => time >= a.startTime && time < a.endTime && a.objectName === 'slider') - let flIndex = 0; + if(currentSlider){ + ctx.globalAlpha = 0.8; + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + } - if(combo >= 100) - flIndex = 1; - else if(combo >= 200) - flIndex = 2; + do{ + const newFrame = beatmap.ScoringFrames[previousFramesIndex]; - const flImage = flImages[flIndex]; + if(newFrame == null) + break; - const cursorPos = playfieldPosition(current.x, current.y); + if(newFrame.offset > time) + break; - ctx.drawImage(flImage, cursorPos[0] - flImage.width / 2, cursorPos[1] - flImage.height / 2); + currentFrame = newFrame; - const currentSlider = beatmap.hitObjects.find(a => time >= a.startTime && time < a.endTime && a.objectName === 'slider') + scoringFrames.push(currentFrame); - if(currentSlider){ - ctx.globalAlpha = 0.8; - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } - } + previousFramesIndex++; + }while(currentFrame.offset < time) - do{ - const newFrame = beatmap.ScoringFrames[previousFramesIndex]; + const UR_BAR_WIDTH = 160; + const UR_BAR_HEIGHT = 4; - if(newFrame == null) - break; + const UR_BAR_Y = canvas.height - 35 - (15 * scale_multiplier); - if(newFrame.offset > time) - break; + const UR_BAR_100 = beatmap.HitWindow100 / beatmap.HitWindow50 * UR_BAR_WIDTH; + const UR_BAR_300 = beatmap.HitWindow300 / beatmap.HitWindow50 * UR_BAR_WIDTH; - currentFrame = newFrame; + if(currentFrame != null){ + const comboPosition = [15, canvas.height - 35]; + const accuracyPosition = [canvas.width - 15, 40]; - scoringFrames.push(currentFrame); + ctx.fillStyle = "white"; + ctx.globalAlpha = 1; + ctx.textAlign = "left"; + ctx.textBaseline = "bottom"; + ctx.font = `${32 * scale_multiplier}px monospace`; + ctx.fillText(`${currentFrame.combo}x`, ...comboPosition); - previousFramesIndex++; - }while(currentFrame.offset < time) + let { pp, stars } = currentFrame; - const UR_BAR_WIDTH = 160; - const UR_BAR_HEIGHT = 4; + if(time - currentFrame.offset < 400 && scoringFrames.length > 1){ + let previousFrame; - const UR_BAR_Y = canvas.height - 35 - (15 * scale_multiplier); + for(let i = scoringFrames.length - 1; i > 0; i--){ + previousFrame = scoringFrames[i]; - const UR_BAR_100 = beatmap.HitWindow100 / beatmap.HitWindow50 * UR_BAR_WIDTH; - const UR_BAR_300 = beatmap.HitWindow300 / beatmap.HitWindow50 * UR_BAR_WIDTH; + if(previousFrame.offset <= time - 400 || previousFrame.pp !== currentFrame.pp) + break; + } - if(currentFrame != null){ - const comboPosition = [15, canvas.height - 35]; - const accuracyPosition = [canvas.width - 15, 40]; + const progress = (time - currentFrame.offset) / (time - previousFrame.offset); + const diffPP = currentFrame.pp - previousFrame.pp; + const diffStars = currentFrame.stars - previousFrame.stars; - ctx.fillStyle = "white"; - ctx.globalAlpha = 1; - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; - ctx.font = `${32 * scale_multiplier}px monospace`; - ctx.fillText(`${currentFrame.combo}x`, ...comboPosition); + pp = previousFrame.pp + diffPP * progress; + stars = previousFrame.stars + diffStars * progress; + } - let { pp, stars } = currentFrame; + ctx.textBaseline = "top"; + ctx.font = `${26 * scale_multiplier}px monospace`; + ctx.fillText(`${pp.toFixed(2)}pp`, 15, 45); - if(time - currentFrame.offset < 400 && scoringFrames.length > 1){ - let previousFrame; + ctx.font = `${21 * scale_multiplier}px monospace`; + ctx.fillText(`★${stars.toFixed(2)}`, 15, 47 + 26 * scale_multiplier); - for(let i = scoringFrames.length - 1; i > 0; i--){ - previousFrame = scoringFrames[i]; + let accuracy = 100; - if(previousFrame.offset <= time - 400 || previousFrame.pp !== currentFrame.pp) - break; - } + const totalHits = currentFrame.count50 * 300 + currentFrame.count100 * 300 + currentFrame.count300 * 300 + currentFrame.countMiss * 300; - const progress = (time - currentFrame.offset) / (time - previousFrame.offset); - const diffPP = currentFrame.pp - previousFrame.pp; - const diffStars = currentFrame.stars - previousFrame.stars; + if(totalHits > 0) + accuracy = (currentFrame.count50 * 50 + currentFrame.count100 * 100 + currentFrame.count300 * 300) + / totalHits * 100; - pp = previousFrame.pp + diffPP * progress; - stars = previousFrame.stars + diffStars * progress; - } + ctx.textAlign = "right"; + ctx.textBaseline = "top"; + ctx.font = `${26 * scale_multiplier}px monospace`; + ctx.fillText(`${accuracy.toFixed(2)}%`, ...accuracyPosition); - ctx.textBaseline = "top"; - ctx.font = `${26 * scale_multiplier}px monospace`; - ctx.fillText(`${pp.toFixed(2)}pp`, 15, 45); + const hitCountPosition = [canvas.width - 15, 45 + 26 * scale_multiplier]; - ctx.font = `${21 * scale_multiplier}px monospace`; - ctx.fillText(`★${stars.toFixed(2)}`, 15, 47 + 26 * scale_multiplier); + ctx.font = `${21 * scale_multiplier}px monospace`; + ctx.fillText(`${currentFrame.count100}x100 ${currentFrame.count50}x50`, ...hitCountPosition); - let accuracy = 100; + hitCountPosition[1] += 2 + 21 * scale_multiplier; + ctx.fillText(`${currentFrame.countMiss}xMiss`, ...hitCountPosition); - const totalHits = currentFrame.count50 * 300 + currentFrame.count100 * 300 + currentFrame.count300 * 300 + currentFrame.countMiss * 300; + const urPosition = [canvas.width - 15, canvas.height - 35]; - if(totalHits > 0) - accuracy = (currentFrame.count50 * 50 + currentFrame.count100 * 100 + currentFrame.count300 * 300) - / totalHits * 100; + ctx.textBaseline = "bottom"; + ctx.font = `${26 * scale_multiplier}px monospace`; - ctx.textAlign = "right"; - ctx.textBaseline = "top"; - ctx.font = `${26 * scale_multiplier}px monospace`; - ctx.fillText(`${accuracy.toFixed(2)}%`, ...accuracyPosition); + let urText = 'UR'; + let { ur } = currentFrame; - const hitCountPosition = [canvas.width - 15, 45 + 26 * scale_multiplier]; + if(beatmap.Replay && (beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC') || beatmap.Replay.Mods.includes("HT"))){ + urText = 'cvUR'; - ctx.font = `${21 * scale_multiplier}px monospace`; - ctx.fillText(`${currentFrame.count100}x100 ${currentFrame.count50}x50`, ...hitCountPosition); + if(beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC')) + ur /= 1.5; - hitCountPosition[1] += 2 + 21 * scale_multiplier; - ctx.fillText(`${currentFrame.countMiss}xMiss`, ...hitCountPosition); + if(beatmap.Replay.Mods.includes('HT')) + ur /= 0.75; + } - const urPosition = [canvas.width - 15, canvas.height - 35]; + ctx.fillText(`${ur.toFixed(2)} ${urText}`, ...urPosition); - ctx.textBaseline = "bottom"; - ctx.font = `${26 * scale_multiplier}px monospace`; + /* + ctx.textAlign = "right"; + ctx.fillText(`${time}`, canvas.width - 15, canvas.height - 35); + ctx.fillText(`${currentFrame.offset}`, canvas.width - 15, canvas.height - 65);*/ - let urText = 'UR'; - let { ur } = currentFrame; + ctx.globalAlpha = 0.5; - if(beatmap.Replay && (beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC') || beatmap.Replay.Mods.includes("HT"))){ - urText = 'cvUR'; + ctx.fillStyle = '#ff9100'; + ctx.fillRect(canvas.width / 2 - UR_BAR_WIDTH / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_WIDTH, UR_BAR_HEIGHT); - if(beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC')) - ur /= 1.5; + ctx.fillStyle = '#4dff00'; + ctx.fillRect(canvas.width / 2 - UR_BAR_100 / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_100, UR_BAR_HEIGHT); - if(beatmap.Replay.Mods.includes('HT')) - ur /= 0.75; - } + ctx.fillStyle = '#00e5ff'; + ctx.fillRect(canvas.width / 2 - UR_BAR_300 / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_300, UR_BAR_HEIGHT); - ctx.fillText(`${ur.toFixed(2)} ${urText}`, ...urPosition); + ctx.globalAlpha = 1; - /* - ctx.textAlign = "right"; - ctx.fillText(`${time}`, canvas.width - 15, canvas.height - 35); - ctx.fillText(`${currentFrame.offset}`, canvas.width - 15, canvas.height - 65);*/ + ctx.textAlign = "left"; + ctx.textBaseline = "bottom"; + ctx.font = `${16 * scale_multiplier}px sans-serif`; - ctx.globalAlpha = 0.5; + ctx.fillStyle = 'rgb(255,255,255,0.8)'; - ctx.fillStyle = '#ff9100'; - ctx.fillRect(canvas.width / 2 - UR_BAR_WIDTH / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_WIDTH, UR_BAR_HEIGHT); + ctx.fillText('W.I.P. – scoring not accurate yet', 15, canvas.height - 10); + } - ctx.fillStyle = '#4dff00'; - ctx.fillRect(canvas.width / 2 - UR_BAR_100 / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_100, UR_BAR_HEIGHT); + for(const scoringFrame of scoringFrames){ + if(scoringFrame.hitOffset != null){ + switch(scoringFrame.result){ + case 300: + ctx.fillStyle = '#00e5ff'; + break; + case 100: + ctx.fillStyle = '#4dff00'; + break; + case 50: + ctx.fillStyle = '#ff9100'; + break; + default: + ctx.fillStyle = 'transparent'; + } - ctx.fillStyle = '#00e5ff'; - ctx.fillRect(canvas.width / 2 - UR_BAR_300 / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_300, UR_BAR_HEIGHT); + ctx.globalAlpha = 0.35; - ctx.globalAlpha = 1; + if(time - scoringFrame.offset > 4000) + ctx.globalAlpha *= Math.max(0, 1 - (time - (scoringFrame.offset + 4000)) / 1000); - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; - ctx.font = `${16 * scale_multiplier}px sans-serif`; + let posX = canvas.width / 2; - ctx.fillStyle = 'rgb(255,255,255,0.8)'; + const offsetX = Math.abs(scoringFrame.hitOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); - ctx.fillText('W.I.P. – scoring not accurate yet', 15, canvas.height - 10); - } + if(scoringFrame.hitOffset > 0) + posX += offsetX; + else + posX -= offsetX; - for(const scoringFrame of scoringFrames){ - if(scoringFrame.hitOffset != null){ - switch(scoringFrame.result){ - case 300: - ctx.fillStyle = '#00e5ff'; - break; - case 100: - ctx.fillStyle = '#4dff00'; - break; - case 50: - ctx.fillStyle = '#ff9100'; - break; - default: - ctx.fillStyle = 'transparent'; - } + ctx.fillRect(posX, UR_BAR_Y - 16 / 2, 2, 16); + } - ctx.globalAlpha = 0.35; + if(!(['miss', 50, 100].includes(scoringFrame.result))) + continue; - if(time - scoringFrame.offset > 4000) - ctx.globalAlpha *= Math.max(0, 1 - (time - (scoringFrame.offset + 4000)) / 1000); + if(time - scoringFrame.offset > 750) + continue; - let posX = canvas.width / 2; + ctx.globalAlpha = Math.min(1, 1.5 - (time - scoringFrame.offset) / 750); + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.font = `${30 * scale_multiplier}px sans-serif`; - const offsetX = Math.abs(scoringFrame.hitOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); + const position = scoringFrame.position.slice(); - if(scoringFrame.hitOffset > 0) - posX += offsetX; - else - posX -= offsetX; + if(scoringFrame.result == 'miss'){ + position[1] += (time - scoringFrame.offset) / 750 * 35; - ctx.fillRect(posX, UR_BAR_Y - 16 / 2, 2, 16); - } + ctx.fillStyle = "#f56767"; - if(!(['miss', 50, 100].includes(scoringFrame.result))) - continue; + ctx.fillText('X', ...playfieldPosition(...position)); + continue; + } - if(time - scoringFrame.offset > 750) - continue; + if(scoringFrame.result == 50){ + ctx.fillStyle = "#67b5f5"; - ctx.globalAlpha = Math.min(1, 1.5 - (time - scoringFrame.offset) / 750); - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.font = `${30 * scale_multiplier}px sans-serif`; + ctx.fillText('50', ...playfieldPosition(...position)); + continue; + } - const position = scoringFrame.position.slice(); + if(scoringFrame.result == 100){ + ctx.fillStyle = "#67f575"; - if(scoringFrame.result == 'miss'){ - position[1] += (time - scoringFrame.offset) / 750 * 35; + ctx.fillText('100', ...playfieldPosition(...position)); + continue; + } + } - ctx.fillStyle = "#f56767"; + let scoringFrameOffsets = scoringFrames.filter(a => a.hitOffset != null).map(a => a.hitOffset); - ctx.fillText('X', ...playfieldPosition(...position)); - continue; - } + const avgOffset = scoringFrameOffsets.length > 0 ? scoringFrameOffsets.reduce((a, v, i) => (a * i + v) / (i + 1)) : 0; - if(scoringFrame.result == 50){ - ctx.fillStyle = "#67b5f5"; + let posX = canvas.width / 2; - ctx.fillText('50', ...playfieldPosition(...position)); - continue; - } + const offsetX = Math.abs(avgOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); - if(scoringFrame.result == 100){ - ctx.fillStyle = "#67f575"; + if(avgOffset > 0) + posX += offsetX; + else + posX -= offsetX; - ctx.fillText('100', ...playfieldPosition(...position)); - continue; - } - } + ctx.globalAlpha = 1; + ctx.fillStyle = 'white'; - let scoringFrameOffsets = scoringFrames.filter(a => a.hitOffset != null).map(a => a.hitOffset); + ctx.beginPath(); + ctx.moveTo(posX - 5, UR_BAR_Y - 16 / 2); + ctx.lineTo(posX, UR_BAR_Y - 16 / 2 + 7); + ctx.lineTo(posX + 5, UR_BAR_Y - 16 / 2); + ctx.fill(); - const avgOffset = scoringFrameOffsets.length > 0 ? scoringFrameOffsets.reduce((a, v, i) => (a * i + v) / (i + 1)) : 0; + ctx.fillRect(canvas.width / 2 - 1, UR_BAR_Y - 16 / 2, 2, 16); + } - let posX = canvas.width / 2; + // Draw replay cursor + if(beatmap.Replay){ + let replay_point = getCursorAt(time, beatmap.ReplayInterpolated); - const offsetX = Math.abs(avgOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); + let smokeActive = false; - if(avgOffset > 0) - posX += offsetX; - else - posX -= offsetX; + ctx.globalAlpha = 1; - ctx.globalAlpha = 1; - ctx.fillStyle = 'white'; + for(let i = beatmap.Replay.lastCursor - 1; i > 0; i--){ + const frame = beatmap.Replay.replay_data[i]; + const previousFrame = beatmap.Replay.replay_data[i - 1]; - ctx.beginPath(); - ctx.moveTo(posX - 5, UR_BAR_Y - 16 / 2); - ctx.lineTo(posX, UR_BAR_Y - 16 / 2 + 7); - ctx.lineTo(posX + 5, UR_BAR_Y - 16 / 2); - ctx.fill(); + if(frame.offset > time) + continue; - ctx.fillRect(canvas.width / 2 - 1, UR_BAR_Y - 16 / 2, 2, 16); - } + if(time - frame.offset > 5000) + break; - // Draw replay cursor - if(beatmap.Replay){ - let replay_point = getCursorAt(time, beatmap.ReplayInterpolated); + ctx.lineWidth = 1; + ctx.strokeStyle = "rgba(255,255,255,0.7)"; - let smokeActive = false; + if(options.analyze && previousFrame != null && time - frame.offset < 750){ + const position0 = playfieldPosition(previousFrame.x, previousFrame.y); + const position1 = playfieldPosition(frame.x, frame.y); - ctx.globalAlpha = 1; + ctx.beginPath(); - for(let i = beatmap.Replay.lastCursor - 1; i > 0; i--){ - const frame = beatmap.Replay.replay_data[i]; - const previousFrame = beatmap.Replay.replay_data[i - 1]; + ctx.moveTo(...position0); + ctx.lineTo(...position1); - if(frame.offset > time) - continue; + ctx.stroke(); + } - if(time - frame.offset > 5000) - break; + if(options.analyze && previousFrame != null && time - frame.offset < 750){ + if(((frame.K1 || frame.M1) && !previousFrame.K1 && !previousFrame.M1) + ||((frame.K2 || frame.M2) && !previousFrame.K2 && !previousFrame.M2)){ - ctx.lineWidth = 1; - ctx.strokeStyle = "rgba(255,255,255,0.7)"; + ctx.strokeStyle = "white"; - if(options.analyze && previousFrame != null && time - frame.offset < 750){ - const position0 = playfieldPosition(previousFrame.x, previousFrame.y); - const position1 = playfieldPosition(frame.x, frame.y); + const position = playfieldPosition(frame.x, frame.y); - ctx.beginPath(); + ctx.beginPath(); - ctx.moveTo(...position0); - ctx.lineTo(...position1); + ctx.moveTo(position[0], position[1] - 5); + ctx.lineTo(position[0], position[1] + 5); + ctx.stroke(); - ctx.stroke(); - } + ctx.moveTo(position[0] - 5, position[1]); + ctx.lineTo(position[0] + 5, position[1]); + ctx.stroke(); + } + } - if(options.analyze && previousFrame != null && time - frame.offset < 750){ - if(((frame.K1 || frame.M1) && !previousFrame.K1 && !previousFrame.M1) - ||((frame.K2 || frame.M2) && !previousFrame.K2 && !previousFrame.M2)){ + ctx.lineWidth = 6 * scale_multiplier; + ctx.strokeStyle = "rgba(255,255,255,0.4)"; - ctx.strokeStyle = "white"; + if(frame.S === false && smokeActive){ + if(smokeActive && !options.analyze){ + ctx.stroke(); + smokeActive = false; + } - const position = playfieldPosition(frame.x, frame.y); + continue; + } - ctx.beginPath(); + if(frame.S){ + if(!smokeActive){ + smokeActive = true; + ctx.beginPath(); + ctx.moveTo(...playfieldPosition(frame.x, frame.y)); + }else{ + ctx.lineTo(...playfieldPosition(frame.x, frame.y)); + } + } + } - ctx.moveTo(position[0], position[1] - 5); - ctx.lineTo(position[0], position[1] + 5); - ctx.stroke(); + if(smokeActive && !options.analyze){ + ctx.stroke(); + } - ctx.moveTo(position[0] - 5, position[1]); - ctx.lineTo(position[0] + 5, position[1]); - ctx.stroke(); - } - } + if(replay_point){ + if(beatmap.Replay.auto !== true){ + ctx.globalAlpha = 1; - ctx.lineWidth = 6 * scale_multiplier; - ctx.strokeStyle = "rgba(255,255,255,0.4)"; + const { K1, K2, M1, M2 } = replay_point.current; - if(frame.S === false && smokeActive){ - if(smokeActive && !options.analyze){ - ctx.stroke(); - smokeActive = false; - } + const keyOverlayTop = canvas.height / 2 - (KEY_OVERLAY_SIZE * 4 + KEY_OVERLAY_PADDING * 4) / 2; - continue; - } + ctx.fillStyle = K1 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; + ctx.fillRect(canvas.width - 30, keyOverlayTop, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); - if(frame.S){ - if(!smokeActive){ - smokeActive = true; - ctx.beginPath(); - ctx.moveTo(...playfieldPosition(frame.x, frame.y)); - }else{ - ctx.lineTo(...playfieldPosition(frame.x, frame.y)); - } - } - } + ctx.fillStyle = K2 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; + ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE + KEY_OVERLAY_PADDING, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); - if(smokeActive && !options.analyze){ - ctx.stroke(); - } + ctx.fillStyle = M1 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; + ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE * 2 + KEY_OVERLAY_PADDING * 2, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); - if(replay_point){ - if(beatmap.Replay.auto !== true){ - ctx.globalAlpha = 1; + ctx.fillStyle = M2 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; + ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE * 3 + KEY_OVERLAY_PADDING * 3, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); + } - const { K1, K2, M1, M2 } = replay_point.current; + if(Array.isArray(replay_point.previous) && !options.analyze){ + ctx.globalAlpha = .35; - const keyOverlayTop = canvas.height / 2 - (KEY_OVERLAY_SIZE * 4 + KEY_OVERLAY_PADDING * 4) / 2; + ctx.beginPath(); - ctx.fillStyle = K1 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; - ctx.fillRect(canvas.width - 30, keyOverlayTop, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); + for(const [index, previousFrame] of replay_point.previous.entries()){ + let position = playfieldPosition(previousFrame.x, previousFrame.y); - ctx.fillStyle = K2 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; - ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE + KEY_OVERLAY_PADDING, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); + if(index === 0) + ctx.moveTo(...position); + else + ctx.lineTo(...position); + } - ctx.fillStyle = M1 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; - ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE * 2 + KEY_OVERLAY_PADDING * 2, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); + ctx.lineWidth = 13 * scale_multiplier; + ctx.lineCap = "round"; - ctx.fillStyle = M2 ? 'rgba(255,255,255,0.7)' : 'rgba(255,255,255,0.3)'; - ctx.fillRect(canvas.width - 30, keyOverlayTop + KEY_OVERLAY_SIZE * 3 + KEY_OVERLAY_PADDING * 3, KEY_OVERLAY_SIZE, KEY_OVERLAY_SIZE); - } + if(options.fill) + ctx.strokeStyle = '#fff4ab'; + else + ctx.strokeStyle = 'white'; - if(Array.isArray(replay_point.previous) && !options.analyze){ - ctx.globalAlpha = .35; + ctx.stroke(); + } - ctx.beginPath(); + if(options.fill) + ctx.fillStyle = '#fff460'; + else + ctx.fillStyle = 'white'; - for(const [index, previousFrame] of replay_point.previous.entries()){ - let position = playfieldPosition(previousFrame.x, previousFrame.y); + let { current } = replay_point; - if(index === 0) - ctx.moveTo(...position); - else - ctx.lineTo(...position); - } + let position = playfieldPosition(current.x, current.y); - ctx.lineWidth = 13 * scale_multiplier; - ctx.lineCap = "round"; + ctx.globalAlpha = 1; - if(options.fill) - ctx.strokeStyle = '#fff4ab'; - else - ctx.strokeStyle = 'white'; + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * 13, 0, 2 * Math.PI, false); + ctx.fill(); + } + } + + // Draw playfield border + if(options.border){ + ctx.strokeStyle = "rgb(200,200,200)"; + ctx.lineWidth = 1; + ctx.globalAlpha = 1; + + let position = playfieldPosition(0, 0); + let size = playfieldPosition(PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT); + ctx.strokeRect(...position, size[0] - position[0], size[1] - position[1]); + } + } - ctx.stroke(); - } + let time = start_time; - if(options.fill) - ctx.fillStyle = '#fff460'; - else - ctx.fillStyle = 'white'; + prepareCanvas(size); + //preprocessSliders(); - let { current } = replay_point; + beatmap.ReplayInterpolated = interpolateReplayData(beatmap.Replay); - let position = playfieldPosition(current.x, current.y); + for(let i in images){ + let image_path = images[i]; - ctx.globalAlpha = 1; + images[i] = await new Promise((resolve, reject) => { + let img = new Image(); + img.onload = () => { + resolve(img); + }; - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * 13, 0, 2 * Math.PI, false); - ctx.fill(); - } - } + img.onerror = reject; + img.src = image_path; + }); + } - // Draw playfield border - if(options.border){ - ctx.strokeStyle = "rgb(200,200,200)"; - ctx.lineWidth = 1; - ctx.globalAlpha = 1; + if(end_time){ + while(time < end_time){ + while (WAITING_FOR_SERVER_ACK){ + await new Promise(r => setTimeout(r, 20)); + } - let position = playfieldPosition(0, 0); - let size = playfieldPosition(PLAYFIELD_WIDTH, PLAYFIELD_HEIGHT); - ctx.strokeRect(...position, size[0] - position[0], size[1] - position[1]); - } - } - let time = start_time; + processFrame(time, options); - prepareCanvas(size); - //preprocessSliders(); + let image_data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; - beatmap.ReplayInterpolated = interpolateReplayData(beatmap.Replay); + // Convert rgb with alpha values to pure rgb as gif doesn't support alpha + if(options.type === 'gif'){ + for(let i = 0; i < image_data.length; i += 4){ + if(image_data[i + 3] > 0){ + let scale = Math.round(image_data[i] * image_data[i + 3] / 255); + image_data[i] = scale; + image_data[i + 1] = scale; + image_data[i + 2] = scale; + image_data[i + 3] = 255; + } + } + } - for(let i in images){ - let image_path = images[i]; + // await fs.writeFile(path.resolve(file_path, `${current_frame}.rgba`), Buffer.from(image_data)); + + // process.send(current_frame); + let abuf = Buffer.from(image_data); + + // process.send({ + // worker_id: worker_id, + // data: abuf + // }); + + // log_as_worker("Sending frame data to main thread " + ++frame_counter); + for (let i = 0; i < Math.ceil(abuf.length/32000); i++){ + ipc.of.world.emit('app.framedata', { + worker_id: worker_id, + seqno: i, + video_frame_seqno: current_frame, + last: (i + 1) * 32000 >= abuf.length, + frame_data: abuf.slice(i * 32000, (i+1)*32000).toString('base64') + }); + } + WAITING_FOR_SERVER_ACK = true; - images[i] = await new Promise((resolve, reject) => { - let img = new Image(); - img.onload = () => { - resolve(img); - }; + current_frame += threads; + time += time_frame; + } - img.onerror = reject; - img.src = image_path; - }); - } + ipc.of.world.emit('app.readyToTerminate', 'true'); + }else{ // no end time -> render single frame + processFrame(time, options); - if(end_time){ - while(time < end_time){ - while (WAITING_FOR_SERVER_ACK){ - await new Promise(r => setTimeout(r, 20)); - } + process.send(canvas.toBuffer().toString('base64')); + process.exit(0); + } +} +ipc.config.id = 'worker-' + worker_id; +ipc.config.retry= 200; +ipc.config.sync = true; +// ipc.config.rawBuffer=true; +// ipc.config.encoding ='base64'; +ipc.config.logger = log_as_worker +ipc.config.silent = true; - processFrame(time, options); +ipc.connectTo( + 'world', + ipc.config.socketRoot + ipc.config.appspace + 'world', + function(){ + // ipc.of.world.on( + // 'connect', + // function(){ + // //make a 6 byte buffer for example + // const myBuffer=Buffer.alloc(6).fill(0); + // + // myBuffer.writeUInt16BE(0x02,0); + // myBuffer.writeUInt32BE(0xffeecc,2); + // + // ipc.log('## connected to world ##', ipc.config.delay); + // ipc.of.world.emit( + // myBuffer + // ); + // } + // ); - let image_data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + ipc.of.world.on( + 'connect', + function(){ + ipc.log('## connected to world ##', ipc.config.delay); + ipc.of.world.emit('workerReady', worker_id); + } + ); - // Convert rgb with alpha values to pure rgb as gif doesn't support alpha - if(options.type === 'gif'){ - for(let i = 0; i < image_data.length; i += 4){ - if(image_data[i + 3] > 0){ - let scale = Math.round(image_data[i] * image_data[i + 3] / 255); - image_data[i] = scale; - image_data[i + 1] = scale; - image_data[i + 2] = scale; - image_data[i + 3] = 255; - } - } - } + ipc.of.world.on( + 'setWorker', + function(data){ + log_as_worker("Setting worker id: " + data); + worker_id = data; + // ipc.of.world.emit('app.framedata', {worker_id: worker_id, frame_data: 'sample_data_ignore'}); + } + ) - // await fs.writeFile(path.resolve(file_path, `${current_frame}.rgba`), Buffer.from(image_data)); - - // process.send(current_frame); - let abuf = Buffer.from(image_data); - - // process.send({ - // worker_id: worker_id, - // data: abuf - // }); - - // log_as_worker("Sending frame data to main thread " + ++frame_counter); - for (let i = 0; i < Math.ceil(abuf.length/32000); i++){ - ipc.of.world.emit('app.framedata', { - worker_id: worker_id, - seqno: i, - video_frame_seqno: current_frame, - last: (i + 1) * 32000 >= abuf.length, - frame_data: abuf.slice(i * 32000, (i+1)*32000).toString('base64') - }); - } - WAITING_FOR_SERVER_ACK = true; + ipc.of.world.on('app.framedataAck', function(data){ + // log_as_worker("Received ACK from server"); + }) - current_frame += threads; - time += time_frame; - } + ipc.of.world.on('app.resumeWorking', function(data){ + // log_as_worker("Received ACK from server to resume work"); + WAITING_FOR_SERVER_ACK = false; + }) - ipc.of.world.emit('app.readyToTerminate', 'true'); - }else{ // no end time -> render single frame - processFrame(time, options); + ipc.of.world.on('app.terminate', function(data){ + log_as_worker("Terminating..."); + process.exit(0); + }) - process.send(canvas.toBuffer().toString('base64')); - process.exit(0); - } - } + ipc.of.world.on( + 'receiveWork', + async function (data){ + // log_as_worker(data); + await run_worker_job(data); } ) } ); +process.on('uncaughtException', err => { + helper.error(err); + process.exit(1); +}); - - -let images = { - "arrow": path.resolve(resources, "images", "arrow.svg") -}; - -// process.on('uncaughtException', err => { -// helper.error(err); -// process.exit(1); -// }); - +process.on('message', async obj => { + await run_worker_job(obj) +}); From 0a05569bec85cca3d3b4520d57756b373c7b78c3 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Tue, 28 Dec 2021 07:38:09 +0000 Subject: [PATCH 6/9] add render abort subcommand (W.I.P.) --- commands/render.js | 69 +++++++++++++++++++++++++++++++++++++-- renderer/render_frame.js | 27 ++++++++++++--- renderer/render_worker.js | 10 ++++++ 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/commands/render.js b/commands/render.js index 04505df..b554af8 100644 --- a/commands/render.js +++ b/commands/render.js @@ -2,13 +2,67 @@ const { execFileSync } = require('child_process'); const URL = require('url'); const path = require('path'); const os = require('os'); -const { fork } = require('child_process'); +const process = require('process'); const osu = require('../osu.js'); const helper = require('../helper.js'); const frame = require('../renderer/render_frame.js') const config = require('../config.json'); +function abort_handler(msg, render_args, reject){ + if (render_args.length !== 2) { + reject(`Expected exactly 2 arguments for a render abort, got ${render_args.length} arguments.`); + return; + } + let render_id = render_args.indexOf('abort') ? render_args[0] : render_args[1]; + try{ + render_id = parseInt(render_id); + render_id = render_id.toString(); // this is dumb, fix please + console.log(`Aborting render ${render_id}`); + console.log(helper.getItem("render_queue")); + if (JSON.parse(helper.getItem("render_queue")).hasOwnProperty(render_id)){ + console.log(`${render_id} exists`); + let render = JSON.parse(helper.getItem("render_queue"))[render_id]; + render.abort = true; + update_render(render); + return; + } + reject(`Render '${render_id}' not found.`); + } catch (e) { + reject(`Could not abort render '${render_id}': ${e}`); + } + +} + +function queue_render(){ + let renders = JSON.parse(helper.getItem("render_queue")); + if (renders === null) { + renders = {}; + } + let render_id; + while (true){ + render_id = process.pid * 1000 + Math.floor(Math.random() * 1000); + render_id = render_id.toString(); + if (!renders.hasOwnProperty(render_id)){ + break; + } + } + renders[render_id] = { + "id": render_id, + "status": "queued", + "start_time": Date.now(), + "abort": false + }; + helper.setItem("render_queue", JSON.stringify(renders)); + return renders[render_id]; +} + +function update_render(render){ + let renders = JSON.parse(helper.getItem("render_queue")); + renders[render.id] = render; + helper.setItem("render_queue", JSON.stringify(renders)); +} + module.exports = { command: ['render', 'frame', 'fail'], description: "Render picture or gif of a beatmap at a specific time. Videos 10 seconds or longer are automatically rendered as mp4 video with audio and beatmap background.", @@ -68,6 +122,13 @@ module.exports = { argv.map(arg => arg.toLowerCase()); + console.log(argv); + if (argv.includes('abort')){ + abort_handler(msg, argv.slice(1), reject); + console.log('aborting'); + return; + } + argv.slice(1).forEach(arg => { if(arg.startsWith('+')) mods = arg.substr(1).toUpperCase().match(/.{1,2}/g); @@ -207,10 +268,13 @@ module.exports = { } Promise.resolve(preview_promise).then(previewTime => { + let current_render = queue_render(); if(previewTime) time = previewTime; if(length > 0 || objects){ + current_render.status = "rendering"; + update_render(current_render); resolve(null); frame.get_frames(download_path, time, length * 1000, mods, size, { @@ -236,7 +300,8 @@ module.exports = { bg_opacity, border: false, objects, - msg + msg, + render: current_render, }); }else{ frame.get_frame(download_path, time, mods, [800, 600], { diff --git a/renderer/render_frame.js b/renderer/render_frame.js index 524145e..7f4c90a 100644 --- a/renderer/render_frame.js +++ b/renderer/render_frame.js @@ -420,6 +420,12 @@ async function downloadMedia(options, beatmap, beatmap_path, size, download_path let beatmap, speed_multiplier; +function check_abort(render_id){ + console.log("checking abort"); + let renders = JSON.parse(helper.getItem("render_queue")); + return renders === null ? false : (renders.hasOwnProperty(render_id) ? renders[render_id].abort : false); +} + module.exports = { get_frame: function (beatmap_path, time, enabled_mods, size, options, cb) { let worker = fork(path.resolve(__dirname, 'beatmap_preprocessor.js'), ['--max-old-space-size=512']); @@ -503,11 +509,11 @@ module.exports = { } - const {msg} = options; + const {msg, render} = options; options.msg = null; - const renderStatus = ['– processing beatmap', '– rendering video']; + const renderStatus = ['– processing beatmap', '– rendering video', `[render ID ${render.id}]`]; // noinspection JSCheckFunctionSignatures const renderMessage = await msg.channel.send({embed: {description: renderStatus.join("\n")}}); @@ -726,7 +732,7 @@ module.exports = { if (typeof next_frame === 'undefined') { // helper.log("queue empty... sleeping (waiting on " + next_frame_worker_id + " - video frame #" + frame_counter +")"); // helper.log("waiting on next frame, sleeping..."); - await new Promise(r => setTimeout(r, 50)); + await new Promise(r => setTimeout(r, 20)); continue; } @@ -969,6 +975,12 @@ module.exports = { size }); + let abort_interval = setInterval(() => { + if (check_abort(render.id)){ + ipc.server.emit(socket, 'abort', ''); + } + }, 500); + worker_to_init.on('close', code => { @@ -976,12 +988,17 @@ module.exports = { cb("Error rendering beatmap"); return false; } - + clearInterval(abort_interval); done++; if (done === threads) { // renderStatus[1] = `✓ rendering frames (${((Date.now() - framesProcessStart) / 1000).toFixed(3)}s)`; + let renders = JSON.parse(helper.getItem("render_queue")); + delete renders[render.id]; + helper.setItem("render_queue", JSON.stringify(renders)); + + if (config.debug) console.timeEnd('render beatmap'); ipc.server.stop(); @@ -995,6 +1012,8 @@ module.exports = { ipc.server.on( 'app.framedata', function(data, socket){ + + // todo: implement slowing down of frame tap if video encoder can't keep up let frame_wid = data.worker_id; worker_frame_buffers[frame_wid].push(Buffer.from(data.frame_data, 'base64')); diff --git a/renderer/render_worker.js b/renderer/render_worker.js index a0e2f8e..1a01147 100644 --- a/renderer/render_worker.js +++ b/renderer/render_worker.js @@ -21,6 +21,7 @@ const crypto = require("crypto"); let WAITING_FOR_SERVER_ACK = false; let frame_counter = 0; +let stop = false; let images = { "arrow": path.resolve(resources, "images", "arrow.svg") }; @@ -1175,6 +1176,9 @@ async function run_worker_job(data) { if(end_time){ while(time < end_time){ + if (stop){ + break; + } while (WAITING_FOR_SERVER_ACK){ await new Promise(r => setTimeout(r, 20)); } @@ -1291,6 +1295,8 @@ ipc.connectTo( process.exit(0); }) + stop = false; + ipc.of.world.on( 'receiveWork', async function (data){ @@ -1299,6 +1305,10 @@ ipc.connectTo( } ) + + ipc.of.world.on('abort', async function(){ + stop = true; + }) } ); From 52f3a03aa599a260473257b66b7d07cbea3a4e82 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Tue, 28 Dec 2021 08:38:00 +0000 Subject: [PATCH 7/9] fix workers stuck on waiting ack, premature promise rejection on worker close --- renderer/render_frame.js | 12 ++++++------ renderer/render_worker.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/renderer/render_frame.js b/renderer/render_frame.js index 7f4c90a..1341727 100644 --- a/renderer/render_frame.js +++ b/renderer/render_frame.js @@ -421,7 +421,7 @@ async function downloadMedia(options, beatmap, beatmap_path, size, download_path let beatmap, speed_multiplier; function check_abort(render_id){ - console.log("checking abort"); + // console.log("checking abort"); let renders = JSON.parse(helper.getItem("render_queue")); return renders === null ? false : (renders.hasOwnProperty(render_id) ? renders[render_id].abort : false); } @@ -982,12 +982,7 @@ module.exports = { }, 500); - worker_to_init.on('close', code => { - if (code > 0) { - cb("Error rendering beatmap"); - return false; - } clearInterval(abort_interval); done++; @@ -1003,6 +998,11 @@ module.exports = { console.timeEnd('render beatmap'); ipc.server.stop(); } + + if (code > 0) { + cb("Error rendering beatmap"); + return false; + } }); worker_idx++; diff --git a/renderer/render_worker.js b/renderer/render_worker.js index 1a01147..166740a 100644 --- a/renderer/render_worker.js +++ b/renderer/render_worker.js @@ -1179,7 +1179,7 @@ async function run_worker_job(data) { if (stop){ break; } - while (WAITING_FOR_SERVER_ACK){ + while (WAITING_FOR_SERVER_ACK && !stop){ await new Promise(r => setTimeout(r, 20)); } From 835c5420661fc8e41b1bc8fcfe1f2929ab1eabe3 Mon Sep 17 00:00:00 2001 From: ILW8 Date: Tue, 28 Dec 2021 09:19:17 +0000 Subject: [PATCH 8/9] add render abort message --- renderer/render_frame.js | 18 +++++++++++++++--- renderer/render_worker.js | 4 +--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/renderer/render_frame.js b/renderer/render_frame.js index 1341727..8f0ac34 100644 --- a/renderer/render_frame.js +++ b/renderer/render_frame.js @@ -418,12 +418,18 @@ async function downloadMedia(options, beatmap, beatmap_path, size, download_path return output; } -let beatmap, speed_multiplier; +let beatmap, speed_multiplier, has_aborted; function check_abort(render_id){ - // console.log("checking abort"); let renders = JSON.parse(helper.getItem("render_queue")); - return renders === null ? false : (renders.hasOwnProperty(render_id) ? renders[render_id].abort : false); + // return renders === null ? false : (renders.hasOwnProperty(render_id) ? renders[render_id].abort : false); + if (renders === null){ + return false; + } + if (renders.hasOwnProperty(render_id)){ + return renders[render_id].abort; + } + return false; } module.exports = { @@ -977,6 +983,7 @@ module.exports = { let abort_interval = setInterval(() => { if (check_abort(render.id)){ + has_aborted = true; ipc.server.emit(socket, 'abort', ''); } }, 500); @@ -996,6 +1003,11 @@ module.exports = { if (config.debug) console.timeEnd('render beatmap'); + + if (has_aborted){ + resolveRender(`Aborted render ${render.id}.`); + } + ipc.server.stop(); } diff --git a/renderer/render_worker.js b/renderer/render_worker.js index 166740a..81c7429 100644 --- a/renderer/render_worker.js +++ b/renderer/render_worker.js @@ -1226,8 +1226,6 @@ async function run_worker_job(data) { current_frame += threads; time += time_frame; } - - ipc.of.world.emit('app.readyToTerminate', 'true'); }else{ // no end time -> render single frame processFrame(time, options); @@ -1302,7 +1300,7 @@ ipc.connectTo( async function (data){ // log_as_worker(data); await run_worker_job(data); - + ipc.of.world.emit('app.readyToTerminate', 'true'); } ) From 4e43d728f413194b80fa62e20964fd873de7caeb Mon Sep 17 00:00:00 2001 From: ILW8 Date: Tue, 28 Dec 2021 10:59:48 +0000 Subject: [PATCH 9/9] fix abort render message appearing on every render --- renderer/render_frame.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/renderer/render_frame.js b/renderer/render_frame.js index 8f0ac34..51856ce 100644 --- a/renderer/render_frame.js +++ b/renderer/render_frame.js @@ -418,7 +418,8 @@ async function downloadMedia(options, beatmap, beatmap_path, size, download_path return output; } -let beatmap, speed_multiplier, has_aborted; +let beatmap, speed_multiplier; +let has_aborted = false; function check_abort(render_id){ let renders = JSON.parse(helper.getItem("render_queue"));