diff --git a/commands/render.js b/commands/render.js index 3aa1496..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.", @@ -41,9 +95,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; @@ -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); @@ -107,7 +168,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 +186,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 +202,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); @@ -202,16 +268,40 @@ 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, { 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, + render: current_render, }); }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..51856ce 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,22 @@ const ffmpeg = require('ffmpeg-static'); const unzip = require('unzipper'); const disk = require('diskusage'); -const { execFile, fork, spawn } = require('child_process'); +const EventEmitter = require('events'); + +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 +74,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 +117,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 +148,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 +162,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 +190,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 +208,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 +220,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 +229,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 +249,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 +262,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 +293,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 +305,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 +318,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 +353,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 +381,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; } @@ -391,66 +419,110 @@ async function downloadMedia(options, beatmap, beatmap_path, size, download_path } let beatmap, speed_multiplier; +let has_aborted = false; + +function check_abort(render_id){ + let renders = JSON.parse(helper.getItem("render_queue")); + // 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 = { - 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 + }); + }); + }, + + get_frames: async function (beatmap_path, time, length, enabled_mods, size, options, cb) { + + + + if (config.debug) + console.time('process beatmap'); + + class Queue extends EventEmitter { + constructor(){ + super(); + this.frames = []; + this.emitEvents = false; + this.throttleThreshold = 3; + } + + enableEvents(){this.emitEvents = true;} + disableEvents(){this.emitEvents = false;} - worker.send({ - beatmap, - start_time: time, - options, - size - }); - }); - }, + 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}; + } - get_frames: async function(beatmap_path, time, length, enabled_mods, size, options, cb){ - if(config.debug) - console.time('process beatmap'); - const { msg } = options; + const {msg, render} = options; options.msg = null; - const renderStatus = ['– processing beatmap', '– rendering frames', '– encoding video']; + const renderStatus = ['– processing beatmap', '– rendering video', `[render ID ${render.id}]`]; + // noinspection JSCheckFunctionSignatures const renderMessage = await msg.channel.send({embed: {description: renderStatus.join("\n")}}); const updateRenderStatus = async () => { @@ -461,7 +533,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 +549,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 +570,146 @@ 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 = {}; + + + + worker.on('message', async _beatmap => { + beatmap = _beatmap; + beatmap.hitObjects = _beatmap.hitObjects; // to make IDE shut up about unknown reference + + if (config.debug) + console.timeEnd('process beatmap'); - for(let i = 0; i < beatmap.hitObjects.length; i++){ - if(beatmap.hitObjects[i].startTime >= time){ - objectIndex = i; - break; - } - } + { + 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; + + for (let i = 0; i < beatmap.hitObjects.length; i++) { + if (beatmap.hitObjects[i].startTime >= time) { + objectIndex = i; + break; + } + } - time -= 200; + 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 (beatmap.hitObjects.length > objectIndex + options.objects) + length = beatmap.hitObjects[objectIndex + options.objects].startTime - time + 400; - if(length >= 10 * 1000) - options.type = 'mp4'; + 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)); - } + } 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){ + 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); + 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 start_time = time; + let rnd = Math.round(1e9 * Math.random()); + let file_path; + let fps = options.fps || 60; - let time_max = Math.min(time + length + 1000, lastObjectTime); + let time_scale = 1; - let actual_length = time_max - time; + if (enabled_mods.includes('DT') || enabled_mods.includes('NC')) + time_scale *= 1.5; - let rnd = Math.round(1e9 * Math.random()); - let file_path; - let fps = options.fps || 60; + if (enabled_mods.includes('HT') || enabled_mods.includes('DC')) + time_scale *= 0.75; - let i = 0; - - let time_scale = 1; - - 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(actual_length > 160 * 1000 && actual_length < 210 * 1000) - size = [350, 262]; - else if(actual_length >= 210 * 1000) - size = [180, 128]; + if (!('type' in options)) { + options.type = 'gif'; + } + if (options.type === 'gif') { + fps = 50; + } - if(actual_length > 360 * 1000){ - actual_length = 360 * 1000; - max_time = time + actual_length; - } + let time_frame = 1000 / fps * time_scale; + let bitrate = 500 * 1024; - file_path = path.resolve(config.frame_path != null ? config.frame_path : os.tmpdir(), 'frames', `${rnd}`); - await fs.promises.mkdir(file_path, { recursive: true }); + 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 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) => { - 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 +721,338 @@ 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("################### entering newPipeFrameLoop #######################"); + let next_frame_worker_id = 0; + let frame_counter = 0; + while (frame_counter < amount_frames) { + 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') { + // 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, 20)); + continue; + } - if(info.available * 0.9 < frames_size){ - resolveRender("Not enough disk space").catch(console.error); + // let next_frame_buffer = Buffer.from(next_frame); + // helper.log(next_frame_buffer.length); + ffmpegProcess.stdin.write(next_frame, err => { + if (err) { + helper.error("something bad happened on video encoder"); + helper.error(err); + callback(null); + } + }); - return false; - } + frame_counter++; - let ffmpeg_args = [ - '-f', 'rawvideo', '-r', fps, '-s', size.join('x'), '-pix_fmt', 'rgba', - '-c:v', 'rawvideo', '-thread_queue_size', 1024, - '-i', 'pipe:0' - ]; + if (frame_counter === amount_frames){ + ffmpegProcess.stdin.end(); + callback(null); + return; + } - 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); + 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); - if(options.type == 'mp4') - bitrate = Math.min(bitrate, (0.7 * MAX_SIZE) * 8 / (actual_length / 1000) / 1024); + // await new Promise(r => setTimeout(r, 5)); + } + + } + + + let ffmpeg_args = ['-f', 'rawvideo', '-r', fps, '-s', size.join('x'), '-pix_fmt', 'rgba', + '-c:v', 'rawvideo', '-thread_queue_size', 1024, + '-i', 'pipe:0']; + + 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 done = 0; + + if (config.debug) + console.time('render beatmap'); + + if (options.type === 'gif') { + if (config.debug) + console.time('encode video'); - let workers = []; + ffmpeg_args.push(`${file_path}/video.gif`); - for(let i = 0; i < threads; i++){ - workers.push( - fork(path.resolve(__dirname, 'render_worker.js')) - ); - } + const encodingProcessStart = Date.now(); - let done = 0; + let ffmpegProcess = spawn(ffmpeg, ffmpeg_args, {shell: true}); - if(config.debug) - console.time('render beatmap'); + ffmpegProcess.on('close', async code => { + if (code > 0) { + resolveRender("Error encoding video") + .then(() => { + fs.promises.rm(file_path, {recursive: true}).catch(helper.error); + }).catch(console.error); + + return false; + } + + if (config.debug) + console.timeEnd('encode video'); + + // renderStatus[1] = `✓ encoding video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; + + 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); + }); - if(options.type == 'gif'){ - if(config.debug) + 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; + } + }); + } 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); + + 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(`${file_path}/video.gif`); + 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(); - let ffmpegProcess = spawn(ffmpeg, ffmpeg_args, { shell: true }); + let ffmpegProcess = spawn(ffmpeg, ffmpeg_args, {shell: true}); - ffmpegProcess.on('close', async code => { - if(code > 0){ + ffmpegProcess.on('close', code => { + if (code > 0) { resolveRender("Error encoding video") - .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); return false; } - if(config.debug) + if (config.debug) console.timeEnd('encode video'); - renderStatus[1] = `✓ encoding video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; + renderStatus[1] = `✓ rendering video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; - resolveRender({files: [{ - attachment: `${file_path}/video.${options.type}`, - name: `video.${options.type}` - }]}).then(() => { - fs.promises.rm(file_path, { recursive: true }).catch(helper.error); + 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); }); - pipeFrameLoop(ffmpegProcess, err => { - if(err){ - resolveRender("Error encoding video") - .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; + + // helper.log(line); + const frame = parseInt(line.substring(6).trim()); + + renderStatus[1] = `– rendering video (${Math.round(frame / amount_frames * 100)}%)`; + }); + + newPipeFrameLoop(ffmpegProcess, err => { + if (err) { + resolveRender("Error encoding video"); return false; } - }); - }else{ - Promise.all([mediaPromise, audioProcessingPromise]).then(response => { - let media = response[0]; - let audio = response[1]; + }).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; + // } + // }); + }); + } - 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")}`); + const framesProcessStart = Date.now(); + helper.log("Expected number of frames: " + amount_frames); - 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'); + let workers = []; + for (let i = 0; i < threads; i++) { + workers.push( + fork(path.resolve(__dirname, 'render_worker.js')) + ); + } - 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` - ); - const encodingProcessStart = Date.now(); + let log_as_server = function(message) { + helper.log("[main thread] " + message); + } - let ffmpegProcess = spawn(ffmpeg, ffmpeg_args, { shell: true }); + 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_queues[worker_idx].enableEvents(); + worker_frame_queues[worker_idx].on('unthrottle', function(){ + ipc.server.emit(socket, 'app.resumeWorking', 'true'); + }); - ffmpegProcess.on('close', code => { - if(code > 0){ - resolveRender("Error encoding video") - .then(() => { - fs.promises.rm(file_path, { recursive: true }).catch(helper.error); - }).catch(console.error); + 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 + }); - return false; - } + let abort_interval = setInterval(() => { + if (check_abort(render.id)){ + has_aborted = true; + ipc.server.emit(socket, 'abort', ''); + } + }, 500); - if(config.debug) - console.timeEnd('encode video'); - renderStatus[2] = `✓ encoding video (${((Date.now() - encodingProcessStart) / 1000).toFixed(3)}s)`; + worker_to_init.on('close', code => { + clearInterval(abort_interval); + done++; - 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); - }); + if (done === threads) { + // renderStatus[1] = `✓ rendering frames (${((Date.now() - framesProcessStart) / 1000).toFixed(3)}s)`; - ffmpegProcess.stderr.on('data', data => { - const line = data.toString(); + let renders = JSON.parse(helper.getItem("render_queue")); + delete renders[render.id]; + helper.setItem("render_queue", JSON.stringify(renders)); - if(!line.startsWith('frame=')) - return; - const frame = parseInt(line.substring(6).trim()); + if (config.debug) + console.timeEnd('render beatmap'); - renderStatus[2] = `– encoding video (${Math.round(frame/amount_frames*100)}%)`; - }); + if (has_aborted){ + resolveRender(`Aborted render ${render.id}.`); + } - pipeFrameLoop(ffmpegProcess, err => { - if(err){ - resolveRender("Error encoding video") - .then(() => { - fs.promises.rm(file_path, { recursive: true }).catch(helper.error); - }).catch(console.error); + ipc.server.stop(); + } + + if (code > 0) { + cb("Error rendering beatmap"); + return false; + } + }); - return false; + 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')); + ipc.server.emit(socket, 'app.framedataAck', 'ACK'); + if(data.last){ + // 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); + if(worker_frame_queues[frame_wid].length() < worker_frame_queues[frame_wid].throttleThreshold){ + ipc.server.emit(socket, 'app.resumeWorking', 'true'); + } } - }); - }); + }) + + ipc.server.on( + 'app.readyToTerminate', + function(data, socket){ + ipc.server.emit(socket, 'app.terminate', ''); + }) } + ); - 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)}%)`; - }); + ipc.server.start(); - worker.on('close', code => { - if(code > 0){ - cb("Error rendering beatmap"); - return false; - } - - done++; - if(done == threads){ - renderStatus[1] = `✓ rendering frames (${((Date.now() - framesProcessStart) / 1000).toFixed(3)}s)`; - if(config.debug) - console.timeEnd('render beatmap'); - } - }); - }); - }); - }); - } + }); + } }; diff --git a/renderer/render_worker.js b/renderer/render_worker.js index 5e33d57..81c7429 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,1306 @@ 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 stop = false; let images = { - "arrow": path.resolve(resources, "images", "arrow.svg") + "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; + } + + // 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 = []; -process.on('uncaughtException', err => { - helper.error(err); - process.exit(1); -}); + let i = scoringFrames.findIndex(a => a.offset > timestamp - 2000); -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 = []; + 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 i = scoringFrames.findIndex(a => a.offset > timestamp - 2000); + let current = replay.replay_data[replay.lastCursor - 1]; + let next = replay.replay_data[replay.lastCursor]; + let previous = []; - while(scoringFrames[i].offset <= timestamp){ - output.push(scoringFrames[i]); - i++; - } + for(let i = 0; i < Math.min(replay.lastCursor - 2, 20); i++) + previous.push(replay.replay_data[replay.lastCursor - i]); - return output; - } + 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 getCursorAt(timestamp, replay){ - while(replay.lastCursor < replay.replay_data.length && replay.replay_data[replay.lastCursor].offset <= timestamp){ - replay.lastCursor++; - } + return {previous, current, next}; + } - let current = replay.replay_data[replay.lastCursor - 1]; - let next = replay.replay_data[replay.lastCursor]; - let previous = []; + function vectorDistance(hitObject1, hitObject2){ + return Math.sqrt((hitObject2[0] - hitObject1[0]) * (hitObject2[0] - hitObject1[0]) + + (hitObject2[1] - hitObject1[1]) * (hitObject2[1] - hitObject1[1])); + } - for(let i = 0; i < Math.min(replay.lastCursor - 2, 20); i++) - previous.push(replay.replay_data[replay.lastCursor - i]); + function processFrame(time, options){ + let hitObjectsOnScreen = []; - 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.globalAlpha = 1; - return {previous, current, next}; - } + // 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); + }); - function vectorDistance(hitObject1, hitObject2){ - return Math.sqrt((hitObject2[0] - hitObject1[0]) * (hitObject2[0] - hitObject1[0]) - + (hitObject2[1] - hitObject1[1]) * (hitObject2[1] - hitObject1[1])); - } + ctx.clearRect(0, 0, canvas.width, canvas.height); - function processFrame(time, options){ - let hitObjectsOnScreen = []; + if(options.black){ + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } - ctx.globalAlpha = 1; + hitObjectsOnScreen.sort(function(a, b){ return a.startTime - b.startTime; }); - // 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.strokeStyle = 'rgba(255,255,255,0.3)'; + ctx.lineWidth = 3 * scale_multiplier; + ctx.shadowColor = 'transparent'; - ctx.clearRect(0, 0, canvas.width, canvas.height); + // Draw follow points + hitObjectsOnScreen.forEach(function(hitObject, index){ + if(index < hitObjectsOnScreen.length - 1){ + let nextObject = hitObjectsOnScreen[index + 1]; - if(options.black){ - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } + if(isNaN(hitObject.endPosition) && isNaN(nextObject.position)) + return false; - hitObjectsOnScreen.sort(function(a, b){ return a.startTime - b.startTime; }); + let distance = vectorDistance(hitObject.endPosition, nextObject.position); - ctx.strokeStyle = 'rgba(255,255,255,0.3)'; - ctx.lineWidth = 3 * scale_multiplier; - ctx.shadowColor = 'transparent'; + 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); - // Draw follow points - hitObjectsOnScreen.forEach(function(hitObject, index){ - if(index < hitObjectsOnScreen.length - 1){ - let nextObject = hitObjectsOnScreen[index + 1]; + let progress_0 = nextObject.startTime - beatmap.TimePreempt - if(isNaN(hitObject.endPosition) && isNaN(nextObject.position)) - return false; + let a = progress_0; - let distance = vectorDistance(hitObject.endPosition, nextObject.position); + progress_0 += time - progress_0; + let progress_1 = nextObject.startTime - beatmap.TimeFadein; - 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); + progress_1 -= a + progress_0 -= a; - let progress_0 = nextObject.startTime - beatmap.TimePreempt + let progress = Math.min(1, progress_0 / progress_1 * 2); - let a = progress_0; + let v = vectorSubtract(end_position, start_position); - progress_0 += time - progress_0; - let progress_1 = nextObject.startTime - beatmap.TimeFadein; + v[0] *= progress; + v[1] *= progress; - progress_1 -= a - progress_0 -= a; - let progress = Math.min(1, progress_0 / progress_1 * 2); + ctx.beginPath(); + ctx.moveTo(...start_position); + ctx.lineTo(vectorAdd(start_position, v[0])); + ctx.stroke(); - let v = vectorSubtract(end_position, start_position); + //then shift x by cos(angle)*radius and y by sin(angle)*radius (TODO) + } + } + }); - v[0] *= progress; - v[1] *= progress; + // Sort hit objects from latest to earliest so the earliest hit object is at the front + hitObjectsOnScreen.reverse(); + 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); - ctx.beginPath(); - ctx.moveTo(...start_position); - ctx.lineTo(vectorAdd(start_position, v[0])); - ctx.stroke(); + if(hitObject.objectName !== 'circle') + opacity = 1 - (time - hitObject.endTime) / 200; - //then shift x by cos(angle)*radius and y by sin(angle)*radius (TODO) - } - } - }); + // Calculate relative approach circle size (number from 0 to 1) + let approachCircle = 1 - (time - (hitObject.startTime - beatmap.TimeFadein)) / beatmap.TimeFadein; - // Sort hit objects from latest to earliest so the earliest hit object is at the front - hitObjectsOnScreen.reverse(); + if(approachCircle < 0) approachCircle = 0; + if(opacity > 1) opacity = 1; - 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); + ctx.shadowBlur = 4 * scale_multiplier; + ctx.fillStyle = "rgba(40,40,40,0.2)"; - if(hitObject.objectName != 'circle') - opacity = 1 - (time - hitObject.endTime) / 200; + let followpoint_index; + let followpoint_progress = 0; - // Calculate relative approach circle size (number from 0 to 1) - let approachCircle = 1 - (time - (hitObject.startTime - beatmap.TimeFadein)) / beatmap.TimeFadein; + // Draw slider + if(hitObject.objectName === "slider"){ + let sliderOpacity = opacity; - if(approachCircle < 0) approachCircle = 0; - if(opacity > 1) opacity = 1; + if(options.hidden){ + const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; - ctx.shadowBlur = 4 * scale_multiplier; - ctx.fillStyle = "rgba(40,40,40,0.2)"; + if(time >= fadeOutStartTime) + sliderOpacity = 1 - (time - fadeOutStartTime) / (hitObject.endTime - fadeOutStartTime); - let followpoint_index; - let followpoint_progress = 0; + if(sliderOpacity < 0) + sliderOpacity = 0; + } - // Draw slider - if(hitObject.objectName == "slider"){ - let sliderOpacity = opacity; + ctx.globalAlpha = sliderOpacity; - 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; - } + ctx.lineWidth = 5 * scale_multiplier; + ctx.strokeStyle = "rgba(255,255,255,0.7)"; - ctx.globalAlpha = sliderOpacity; + let snakingStart = hitObject.startTime - beatmap.TimePreempt; + let snakingFinish = hitObject.startTime - beatmap.TimeFadein; - ctx.lineWidth = 5 * scale_multiplier; - ctx.strokeStyle = "rgba(255,255,255,0.7)"; + let snakingProgress = Math.max(0, Math.min(1, (time - snakingStart) / (snakingFinish - snakingStart))); - let snakingStart = hitObject.startTime - beatmap.TimePreempt; - let snakingFinish = hitObject.startTime - beatmap.TimeFadein; + let render_dots = []; - let snakingProgress = Math.max(0, Math.min(1, (time - snakingStart) / (snakingFinish - snakingStart))); + for(let x = 0; x < Math.floor(hitObject.SliderDots.length * snakingProgress); x++) + render_dots.push(hitObject.SliderDots[x]); - let render_dots = []; + // 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" - for(let x = 0; x < Math.floor(hitObject.SliderDots.length * snakingProgress); x++) - render_dots.push(hitObject.SliderDots[x]); + ctx.lineWidth = scale_multiplier * beatmap.Radius * 2 - // 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" + // 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.lineWidth = scale_multiplier * beatmap.Radius * 2 + if(x === 0){ + ctx.moveTo(...position); + }else{ + ctx.lineTo(...position); + } + } - // 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.stroke(); - if(x == 0){ - ctx.moveTo(...position); - }else{ - ctx.lineTo(...position); - } - } + ctx.lineWidth = scale_multiplier * (beatmap.Radius * 2 - 10); + ctx.strokeStyle = 'rgba(0,0,0,0.6)'; - ctx.stroke(); + ctx.stroke(); - ctx.lineWidth = scale_multiplier * (beatmap.Radius * 2 - 10); - ctx.strokeStyle = 'rgba(0,0,0,0.6)'; + let currentTurn = 0, currentOffset, currentTurnStart; - ctx.stroke(); + // 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 currentTurn = 0, currentOffset, currentTurnStart; + let dot_index = 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; + if(currentTurn % 2 === 0) + dot_index = currentOffset * hitObject.SliderDots.length; + else + dot_index = (1 - currentOffset) * hitObject.SliderDots.length; - let dot_index = 0; + followpoint_index = Math.floor(dot_index); - if(currentTurn % 2 == 0) - dot_index = currentOffset * hitObject.SliderDots.length; - else - dot_index = (1 - currentOffset) * hitObject.SliderDots.length; + /* 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; + } + } - followpoint_index = Math.floor(dot_index); + // Render slider ticks (WIP) + if(time <= hitObject.endTime){ + ctx.strokeStyle = 'rgba(255,255,255,0.8)'; + ctx.lineWidth = 5 * scale_multiplier; - /* 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 slider_ticks = hitObject.SliderTicks.slice(); - // Render slider ticks (WIP) - if(time <= hitObject.endTime){ - ctx.strokeStyle = 'rgba(255,255,255,0.8)'; - ctx.lineWidth = 5 * scale_multiplier; + // Reverse slider ticks depending on current slider direction + if(currentTurn > 0 && currentTurn % 2 !== 0) + slider_ticks.reverse(); - let slider_ticks = hitObject.SliderTicks.slice(); + let max = Math.floor(slider_ticks.length * snakingProgress); - // Reverse slider ticks depending on current slider direction - if(currentTurn > 0 && currentTurn % 2 != 0) - slider_ticks.reverse(); + let offset = time - currentTurnStart; - let max = Math.floor(slider_ticks.length * snakingProgress); + 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 offset = time - currentTurnStart; + let tick = slider_ticks[x]; - 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 > 0 && currentTurn % 2 !== 0) + tick.offset = tick.reverseOffset; - let tick = slider_ticks[x]; + if(followpoint_index && tick.offset < offset) + continue; - if(currentTurn > 0 && currentTurn % 2 != 0) - tick.offset = tick.reverseOffset; + let position = playfieldPosition(...tick.position); + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * beatmap.Radius / 5, 0, 2 * Math.PI, false); + ctx.stroke(); + } - if(followpoint_index && tick.offset < offset) - continue; + ctx.globalAlpha = opacity; + } - let position = playfieldPosition(...tick.position); - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * beatmap.Radius / 5, 0, 2 * Math.PI, false); - ctx.stroke(); - } + // 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.globalAlpha = opacity; - } + let timeSince = Math.max(0, Math.min(1, (time - repeatOffset) / 200)); - // 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; + if(time >= repeatOffset) + ctx.globalAlpha = (1 - timeSince); + else + ctx.globalAlpha = Math.min(1, Math.max(0, (time - fadeInStart) / 50)); - let timeSince = Math.max(0, Math.min(1, (time - repeatOffset) / 200)); + let sizeFactor = 1 + timeSince * 0.3; - if(time >= repeatOffset) - ctx.globalAlpha = (1 - timeSince); - else - ctx.globalAlpha = Math.min(1, Math.max(0, (time - fadeInStart) / 50)); + let comparePosition = + (x - 1) % 2 === 0 ? hitObject.SliderDots[hitObject.SliderDots.length - 2] : hitObject.SliderDots[1]; - let sizeFactor = 1 + timeSince * 0.3; + let repeatDirection = Math.atan2(comparePosition[1] - repeatPosition[1], comparePosition[0] - repeatPosition[0]); - let comparePosition = - (x - 1) % 2 == 0 ? hitObject.SliderDots[hitObject.SliderDots.length - 2] : hitObject.SliderDots[1]; + let position = playfieldPosition(...repeatPosition); - let repeatDirection = Math.atan2(comparePosition[1] - repeatPosition[1], comparePosition[0] - repeatPosition[0]); + let size = beatmap.Radius * 2 * scale_multiplier; - let position = playfieldPosition(...repeatPosition); + ctx.save(); - let size = beatmap.Radius * 2 * scale_multiplier; + ctx.translate(...position); + ctx.rotate(repeatDirection); - ctx.save(); + position = [0, 0]; - ctx.translate(...position); - ctx.rotate(repeatDirection); + ctx.lineWidth = 5 * scale_multiplier; + ctx.beginPath(); + ctx.strokeStyle = "rgba(255,255,255,0.85)"; - position = [0, 0]; + if(!options.noshadow) + ctx.shadowColor = "rgba(0,0,0,0.7)"; - ctx.lineWidth = 5 * scale_multiplier; - ctx.beginPath(); - ctx.strokeStyle = "rgba(255,255,255,0.85)"; + // 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(!options.noshadow) - ctx.shadowColor = "rgba(0,0,0,0.7)"; + // 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, sizeFactor * 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, sizeFactor * 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 = 18; - fontSize += 16 * (1 - (beatmap.CircleSize / 10)); + // Draw combo number on circle + ctx.font = `${fontSize}px sans-serif`; - fontSize *= scale_multiplier * sizeFactor; + ctx.fillText("➤", ...position); - // Draw combo number on circle - ctx.font = `${fontSize}px sans-serif`; + /* this doesn't render correctly for some reason??? + using text for now I guess (TODO: FIX) */ + //ctx.drawImage(images.arrow, ...position, size, size); - ctx.fillText("➤", ...position); + ctx.restore(); + } - /* this doesn't render correctly for some reason??? - using text for now I guess (TODO: FIX) */ - //ctx.drawImage(images.arrow, ...position, size, size); + ctx.globalAlpha = opacity; + } - ctx.restore(); - } + let circleOpacity = opacity; - ctx.globalAlpha = opacity; - } + if(options.hidden && circleOpacity >= 1){ + const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; - let circleOpacity = opacity; + if(time >= fadeOutStartTime) + circleOpacity = 1 - (time - fadeOutStartTime) / (beatmap.TimePreempt * 0.3); - if(options.hidden && circleOpacity >= 1){ - const fadeOutStartTime = hitObject.startTime - beatmap.TimePreempt + beatmap.TimeFadein; + if(circleOpacity < 0) + circleOpacity = 0; + } - if(time >= fadeOutStartTime) - circleOpacity = 1 - (time - fadeOutStartTime) / (beatmap.TimePreempt * 0.3); + ctx.globalAlpha = circleOpacity; - if(circleOpacity < 0) - circleOpacity = 0; - } + // Draw circles or slider heads + if(hitObject.objectName !== "spinner"){ + ctx.lineWidth = 5 * scale_multiplier; + ctx.beginPath(); + ctx.strokeStyle = "rgba(255,255,255,0.85)"; - ctx.globalAlpha = circleOpacity; + if(time < hitObject.startTime){ + if(!options.noshadow) + ctx.shadowColor = "rgba(0,0,0,0.7)"; - // 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 = playfieldPosition(...hitObject.position); - 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, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); + ctx.fill(); + } - let position = playfieldPosition(...hitObject.position); + // Draw circle border + ctx.beginPath(); + ctx.arc(...position, 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 = 16; + fontSize += 16 * (1 - (beatmap.CircleSize / 10)); - ctx.fillStyle = 'white'; - ctx.textBaseline = "middle"; - ctx.textAlign = "center"; + fontSize *= scale_multiplier; - let fontSize = 16; - fontSize += 16 * (1 - (beatmap.CircleSize / 10)); + // Draw combo number on circle + ctx.font = `${fontSize}px sans-serif`; + ctx.fillText(hitObject.ComboNumber, position[0], position[1]); - fontSize *= scale_multiplier; + // 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 combo number on circle - ctx.font = `${fontSize}px sans-serif`; - ctx.fillText(hitObject.ComboNumber, position[0], position[1]); + // 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]; - // 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(hitObject.SliderDots.length - 1 > followpoint_index){ + // Interpolate follow point position - // 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 pos_next = hitObject.SliderDots[followpoint_index + 1]; - if(hitObject.SliderDots.length - 1 > followpoint_index){ - // Interpolate follow point position + let distance = vectorDistance(pos_current, pos_next); - let pos_next = hitObject.SliderDots[followpoint_index + 1]; + let n = Math.max(1, followpoint_progress * distance); - let distance = vectorDistance(pos_current, pos_next); + 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]) + ] + } + } - let n = Math.max(1, followpoint_progress * distance); + ctx.globalAlpha = 1; - 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]) - ] - } - } + let position; - ctx.globalAlpha = 1; + // Draw follow point in circle - let position; + ctx.fillStyle = "rgba(255,255,255,0.3)"; + ctx.beginPath(); - // Draw follow point in circle + position = playfieldPosition(...pos_current); + ctx.arc(...position, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); + ctx.fill(); - ctx.fillStyle = "rgba(255,255,255,0.3)"; - ctx.beginPath(); + // Draw follow circle visible around the follow point - position = playfieldPosition(...pos_current); - ctx.arc(...position, scale_multiplier * beatmap.Radius, 0, 2 * Math.PI, false); - ctx.fill(); + 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) + ]; + } + } + */ - // Draw follow circle visible around the follow point + // Outer spinner circle + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * 240, 0, 2 * Math.PI, false); + ctx.stroke(); - 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) - ]; - } - } - */ + // Inner spinner circle + ctx.beginPath(); + ctx.arc(...position, scale_multiplier * 30, 0, 2 * Math.PI, false); + ctx.stroke(); + } + } - // Outer spinner circle - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * 240, 0, 2 * Math.PI, false); - ctx.stroke(); + 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; - // Inner spinner circle - ctx.beginPath(); - ctx.arc(...position, scale_multiplier * 30, 0, 2 * Math.PI, false); - ctx.stroke(); - } - } + if(beatmap.Replay.auto !== true){ + if(hitObject.hitOffset == null) + hitOffset += beatmap.HitWindow50; + else + hitOffset += hitObject.hitOffset; + } - 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 timeSince = Math.min(1, Math.max(0, (time - (hitObject.startTime + hitOffset)) / 200)); + let opacity = 1 - timeSince; + let sizeFactor = 1 + timeSince * 0.3; - if(beatmap.Replay.auto !== true){ - if(hitObject.hitOffset == null) - hitOffset += beatmap.HitWindow50; - else - hitOffset += hitObject.hitOffset; - } + ctx.globalAlpha = opacity; - let timeSince = Math.min(1, Math.max(0, (time - (hitObject.startTime + hitOffset)) / 200)); - let opacity = 1 - timeSince; - let sizeFactor = 1 + timeSince * 0.3; + if(!options.noshadow) + ctx.shadowColor = "rgba(0,0,0,0.7)"; - ctx.globalAlpha = opacity; + ctx.lineWidth = 6 * scale_multiplier; + ctx.beginPath(); + ctx.strokeStyle = "rgba(255,255,255,0.85)"; - if(!options.noshadow) - ctx.shadowColor = "rgba(0,0,0,0.7)"; + let position = playfieldPosition(...hitObject.position); - ctx.lineWidth = 6 * scale_multiplier; - ctx.beginPath(); - ctx.strokeStyle = "rgba(255,255,255,0.85)"; + 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); + ctx.beginPath(); + ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); + ctx.stroke(); - 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.fillStyle = 'white'; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; - ctx.beginPath(); - ctx.arc(...position, sizeFactor * scale_multiplier * beatmap.Radius - ctx.lineWidth / 2, 0, 2 * Math.PI, false); - ctx.stroke(); + let fontSize = 16; + 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)); + ctx.font = `${fontSize}px sans-serif`; + ctx.fillText(hitObject.ComboNumber, ...position); + } + } + }); - fontSize *= scale_multiplier * sizeFactor; + if(options.analyze){ + for(const hitObject of beatmap.hitObjects){ + if(hitObject.objectName === 'spinner') + continue; - ctx.font = `${fontSize}px sans-serif`; - ctx.fillText(hitObject.ComboNumber, ...position); - } - } - }); + if(hitObject.hitResult > 0 && hitObject.objectName === 'circle' + || hitObject.MissedSliderStart < 1 && hitObject.objectName === 'slider') + continue; - if(options.analyze){ - for(const hitObject of beatmap.hitObjects){ - if(hitObject.objectName == 'spinner') - continue; + if(time < hitObject.startTime) + continue; - if(hitObject.hitResult > 0 && hitObject.objectName == 'circle' - || hitObject.MissedSliderStart < 1 && hitObject.objectName == 'slider') - continue; + if(time - hitObject.startTime > 750) + continue; - if(time < hitObject.startTime) - continue; + const position = playfieldPosition(...hitObject.position); - if(time - hitObject.startTime > 750) - continue; + 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 position = playfieldPosition(...hitObject.position); + if(beatmap.ScoringFrames && beatmap.Replay.auto !== true){ + //const scoringFrames = getScoringFrames(time, beatmap.ScoringFrames); - 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 previousFramesIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time - 5000); - if(beatmap.ScoringFrames && beatmap.Replay.auto !== true){ - //const scoringFrames = getScoringFrames(time, beatmap.ScoringFrames); + let currentFrameIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time) - 1; - let previousFramesIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time - 5000); + let currentFrame = beatmap.ScoringFrames[currentFrameIndex]; - let currentFrameIndex = beatmap.ScoringFrames.findIndex(a => a.offset >= time) - 1; + if(currentFrame == null) + currentFrame = beatmap.ScoringFrames[beatmap.ScoringFrames.length - 1]; - let currentFrame = beatmap.ScoringFrames[currentFrameIndex]; + const scoringFrames = []; - if(currentFrame == null) - currentFrame = beatmap.ScoringFrames[beatmap.ScoringFrames.length - 1]; + if(options.flashlight){ + ctx.globalAlpha = 1; - const scoringFrames = []; + let { current } = getCursorAt(time, beatmap.ReplayInterpolated); - if(options.flashlight){ - ctx.globalAlpha = 1; + const { combo } = currentFrame; - let { current } = getCursorAt(time, beatmap.ReplayInterpolated); + let flIndex = 0; - const { combo } = currentFrame; + if(combo >= 100) + flIndex = 1; + else if(combo >= 200) + flIndex = 2; - let flIndex = 0; + const flImage = flImages[flIndex]; - if(combo >= 100) - flIndex = 1; - else if(combo >= 200) - flIndex = 2; + const cursorPos = playfieldPosition(current.x, current.y); - const flImage = flImages[flIndex]; + ctx.drawImage(flImage, cursorPos[0] - flImage.width / 2, cursorPos[1] - flImage.height / 2); - const cursorPos = playfieldPosition(current.x, current.y); + const currentSlider = beatmap.hitObjects.find(a => time >= a.startTime && time < a.endTime && a.objectName === 'slider') - ctx.drawImage(flImage, cursorPos[0] - flImage.width / 2, cursorPos[1] - flImage.height / 2); + if(currentSlider){ + ctx.globalAlpha = 0.8; + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + } - const currentSlider = beatmap.hitObjects.find(a => time >= a.startTime && time < a.endTime && a.objectName == 'slider') + do{ + const newFrame = beatmap.ScoringFrames[previousFramesIndex]; - if(currentSlider){ - ctx.globalAlpha = 0.8; - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - } - } + if(newFrame == null) + break; - do{ - const newFrame = beatmap.ScoringFrames[previousFramesIndex]; + if(newFrame.offset > time) + break; - if(newFrame == null) - break; + currentFrame = newFrame; - if(newFrame.offset > time) - break; + scoringFrames.push(currentFrame); - currentFrame = newFrame; + previousFramesIndex++; + }while(currentFrame.offset < time) - scoringFrames.push(currentFrame); + const UR_BAR_WIDTH = 160; + const UR_BAR_HEIGHT = 4; - previousFramesIndex++; - }while(currentFrame.offset < time) + const UR_BAR_Y = canvas.height - 35 - (15 * scale_multiplier); - const UR_BAR_WIDTH = 160; - const UR_BAR_HEIGHT = 4; + const UR_BAR_100 = beatmap.HitWindow100 / beatmap.HitWindow50 * UR_BAR_WIDTH; + const UR_BAR_300 = beatmap.HitWindow300 / beatmap.HitWindow50 * UR_BAR_WIDTH; - const UR_BAR_Y = canvas.height - 35 - (15 * scale_multiplier); + if(currentFrame != null){ + const comboPosition = [15, canvas.height - 35]; + const accuracyPosition = [canvas.width - 15, 40]; - const UR_BAR_100 = beatmap.HitWindow100 / beatmap.HitWindow50 * UR_BAR_WIDTH; - const UR_BAR_300 = beatmap.HitWindow300 / beatmap.HitWindow50 * UR_BAR_WIDTH; + 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(currentFrame != null){ - const comboPosition = [15, canvas.height - 35]; - const accuracyPosition = [canvas.width - 15, 40]; + let { pp, stars } = 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); + if(time - currentFrame.offset < 400 && scoringFrames.length > 1){ + let previousFrame; - let { pp, stars } = currentFrame; + for(let i = scoringFrames.length - 1; i > 0; i--){ + previousFrame = scoringFrames[i]; - if(time - currentFrame.offset < 400 && scoringFrames.length > 1){ - let previousFrame; + if(previousFrame.offset <= time - 400 || previousFrame.pp !== currentFrame.pp) + break; + } - for(let i = scoringFrames.length - 1; i > 0; i--){ - previousFrame = scoringFrames[i]; + const progress = (time - currentFrame.offset) / (time - previousFrame.offset); + const diffPP = currentFrame.pp - previousFrame.pp; + const diffStars = currentFrame.stars - previousFrame.stars; - if(previousFrame.offset <= time - 400 || previousFrame.pp != currentFrame.pp) - break; - } + pp = previousFrame.pp + diffPP * progress; + stars = previousFrame.stars + diffStars * progress; + } - const progress = (time - currentFrame.offset) / (time - previousFrame.offset); - const diffPP = currentFrame.pp - previousFrame.pp; - const diffStars = currentFrame.stars - previousFrame.stars; + ctx.textBaseline = "top"; + ctx.font = `${26 * scale_multiplier}px monospace`; + ctx.fillText(`${pp.toFixed(2)}pp`, 15, 45); - pp = previousFrame.pp + diffPP * progress; - stars = previousFrame.stars + diffStars * progress; - } + ctx.font = `${21 * scale_multiplier}px monospace`; + ctx.fillText(`★${stars.toFixed(2)}`, 15, 47 + 26 * scale_multiplier); - ctx.textBaseline = "top"; - ctx.font = `${26 * scale_multiplier}px monospace`; - ctx.fillText(`${pp.toFixed(2)}pp`, 15, 45); + let accuracy = 100; - ctx.font = `${21 * scale_multiplier}px monospace`; - ctx.fillText(`★${stars.toFixed(2)}`, 15, 47 + 26 * scale_multiplier); - - let accuracy = 100; + const totalHits = currentFrame.count50 * 300 + currentFrame.count100 * 300 + currentFrame.count300 * 300 + currentFrame.countMiss * 300; - const totalHits = currentFrame.count50 * 300 + currentFrame.count100 * 300 + currentFrame.count300 * 300 + currentFrame.countMiss * 300; + if(totalHits > 0) + accuracy = (currentFrame.count50 * 50 + currentFrame.count100 * 100 + currentFrame.count300 * 300) + / totalHits * 100; - if(totalHits > 0) - accuracy = (currentFrame.count50 * 50 + currentFrame.count100 * 100 + currentFrame.count300 * 300) - / totalHits * 100; + ctx.textAlign = "right"; + ctx.textBaseline = "top"; + ctx.font = `${26 * scale_multiplier}px monospace`; + ctx.fillText(`${accuracy.toFixed(2)}%`, ...accuracyPosition); - ctx.textAlign = "right"; - ctx.textBaseline = "top"; - ctx.font = `${26 * scale_multiplier}px monospace`; - ctx.fillText(`${accuracy.toFixed(2)}%`, ...accuracyPosition); + const hitCountPosition = [canvas.width - 15, 45 + 26 * scale_multiplier]; - const hitCountPosition = [canvas.width - 15, 45 + 26 * scale_multiplier]; + ctx.font = `${21 * scale_multiplier}px monospace`; + ctx.fillText(`${currentFrame.count100}x100 ${currentFrame.count50}x50`, ...hitCountPosition); - ctx.font = `${21 * scale_multiplier}px monospace`; - ctx.fillText(`${currentFrame.count100}x100 ${currentFrame.count50}x50`, ...hitCountPosition); + hitCountPosition[1] += 2 + 21 * scale_multiplier; + ctx.fillText(`${currentFrame.countMiss}xMiss`, ...hitCountPosition); - hitCountPosition[1] += 2 + 21 * scale_multiplier; - ctx.fillText(`${currentFrame.countMiss}xMiss`, ...hitCountPosition); + const urPosition = [canvas.width - 15, canvas.height - 35]; - const urPosition = [canvas.width - 15, canvas.height - 35]; + ctx.textBaseline = "bottom"; + ctx.font = `${26 * scale_multiplier}px monospace`; - ctx.textBaseline = "bottom"; - ctx.font = `${26 * scale_multiplier}px monospace`; + let urText = 'UR'; + let { ur } = currentFrame; - let urText = 'UR'; - let { ur } = currentFrame; + if(beatmap.Replay && (beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC') || beatmap.Replay.Mods.includes("HT"))){ + urText = 'cvUR'; - if(beatmap.Replay && (beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC') || beatmap.Replay.Mods.includes("HT"))){ - urText = 'cvUR'; + if(beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC')) + ur /= 1.5; - if(beatmap.Replay.Mods.includes('DT') || beatmap.Replay.Mods.includes('NC')) - ur /= 1.5; + if(beatmap.Replay.Mods.includes('HT')) + ur /= 0.75; + } - if(beatmap.Replay.Mods.includes('HT')) - ur /= 0.75; - } + ctx.fillText(`${ur.toFixed(2)} ${urText}`, ...urPosition); - ctx.fillText(`${ur.toFixed(2)} ${urText}`, ...urPosition); + /* + ctx.textAlign = "right"; + ctx.fillText(`${time}`, canvas.width - 15, canvas.height - 35); + ctx.fillText(`${currentFrame.offset}`, canvas.width - 15, canvas.height - 65);*/ - /* - ctx.textAlign = "right"; - ctx.fillText(`${time}`, canvas.width - 15, canvas.height - 35); - ctx.fillText(`${currentFrame.offset}`, canvas.width - 15, canvas.height - 65);*/ + ctx.globalAlpha = 0.5; - ctx.globalAlpha = 0.5; + 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.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.fillStyle = '#4dff00'; + ctx.fillRect(canvas.width / 2 - UR_BAR_100 / 2, UR_BAR_Y - UR_BAR_HEIGHT / 2, UR_BAR_100, UR_BAR_HEIGHT); - 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); + 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.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 = 1; - ctx.globalAlpha = 1; + ctx.textAlign = "left"; + ctx.textBaseline = "bottom"; + ctx.font = `${16 * scale_multiplier}px sans-serif`; - ctx.textAlign = "left"; - ctx.textBaseline = "bottom"; - ctx.font = `${16 * scale_multiplier}px sans-serif`; + ctx.fillStyle = 'rgb(255,255,255,0.8)'; - ctx.fillStyle = 'rgb(255,255,255,0.8)'; + ctx.fillText('W.I.P. – scoring not accurate yet', 15, canvas.height - 10); + } - ctx.fillText('W.I.P. – scoring not accurate yet', 15, canvas.height - 10); - } + 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'; + } - 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.globalAlpha = 0.35; - ctx.globalAlpha = 0.35; + if(time - scoringFrame.offset > 4000) + ctx.globalAlpha *= Math.max(0, 1 - (time - (scoringFrame.offset + 4000)) / 1000); - if(time - scoringFrame.offset > 4000) - ctx.globalAlpha *= Math.max(0, 1 - (time - (scoringFrame.offset + 4000)) / 1000); + let posX = canvas.width / 2; - let posX = canvas.width / 2; + const offsetX = Math.abs(scoringFrame.hitOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); - const offsetX = Math.abs(scoringFrame.hitOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); + if(scoringFrame.hitOffset > 0) + posX += offsetX; + else + posX -= offsetX; - if(scoringFrame.hitOffset > 0) - posX += offsetX; - else - posX -= offsetX; + ctx.fillRect(posX, UR_BAR_Y - 16 / 2, 2, 16); + } - ctx.fillRect(posX, UR_BAR_Y - 16 / 2, 2, 16); - } + if(!(['miss', 50, 100].includes(scoringFrame.result))) + continue; - if(!(['miss', 50, 100].includes(scoringFrame.result))) - continue; + if(time - scoringFrame.offset > 750) + continue; - if(time - scoringFrame.offset > 750) - continue; + 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.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 position = scoringFrame.position.slice(); - const position = scoringFrame.position.slice(); + if(scoringFrame.result == 'miss'){ + position[1] += (time - scoringFrame.offset) / 750 * 35; - if(scoringFrame.result == 'miss'){ - position[1] += (time - scoringFrame.offset) / 750 * 35; - - ctx.fillStyle = "#f56767"; + ctx.fillStyle = "#f56767"; - ctx.fillText('X', ...playfieldPosition(...position)); - continue; - } + ctx.fillText('X', ...playfieldPosition(...position)); + continue; + } - if(scoringFrame.result == 50){ - ctx.fillStyle = "#67b5f5"; + if(scoringFrame.result == 50){ + ctx.fillStyle = "#67b5f5"; - ctx.fillText('50', ...playfieldPosition(...position)); - continue; - } + ctx.fillText('50', ...playfieldPosition(...position)); + continue; + } - if(scoringFrame.result == 100){ - ctx.fillStyle = "#67f575"; + if(scoringFrame.result == 100){ + ctx.fillStyle = "#67f575"; - ctx.fillText('100', ...playfieldPosition(...position)); - continue; - } - } + ctx.fillText('100', ...playfieldPosition(...position)); + continue; + } + } - let scoringFrameOffsets = scoringFrames.filter(a => a.hitOffset != null).map(a => a.hitOffset); + let scoringFrameOffsets = scoringFrames.filter(a => a.hitOffset != null).map(a => a.hitOffset); - const avgOffset = scoringFrameOffsets.length > 0 ? scoringFrameOffsets.reduce((a, v, i) => (a * i + v) / (i + 1)) : 0; + const avgOffset = scoringFrameOffsets.length > 0 ? scoringFrameOffsets.reduce((a, v, i) => (a * i + v) / (i + 1)) : 0; - let posX = canvas.width / 2; + let posX = canvas.width / 2; - const offsetX = Math.abs(avgOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); + const offsetX = Math.abs(avgOffset) / beatmap.HitWindow50 * (UR_BAR_WIDTH / 2); - if(avgOffset > 0) - posX += offsetX; - else - posX -= offsetX; + if(avgOffset > 0) + posX += offsetX; + else + posX -= offsetX; - ctx.globalAlpha = 1; - ctx.fillStyle = 'white'; + ctx.globalAlpha = 1; + ctx.fillStyle = '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(); + 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.fillRect(canvas.width / 2 - 1, UR_BAR_Y - 16 / 2, 2, 16); - } + ctx.fillRect(canvas.width / 2 - 1, UR_BAR_Y - 16 / 2, 2, 16); + } - // Draw replay cursor - if(beatmap.Replay){ - let replay_point = getCursorAt(time, beatmap.ReplayInterpolated); + // Draw replay cursor + if(beatmap.Replay){ + let replay_point = getCursorAt(time, beatmap.ReplayInterpolated); - let smokeActive = false; + let smokeActive = false; - ctx.globalAlpha = 1; + ctx.globalAlpha = 1; - 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]; + 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.offset > time) - continue; + if(frame.offset > time) + continue; - if(time - frame.offset > 5000) - break; + if(time - frame.offset > 5000) + break; - ctx.lineWidth = 1; - ctx.strokeStyle = "rgba(255,255,255,0.7)"; + ctx.lineWidth = 1; + ctx.strokeStyle = "rgba(255,255,255,0.7)"; - if(options.analyze && previousFrame != null && time - frame.offset < 750){ - const position0 = playfieldPosition(previousFrame.x, previousFrame.y); - const position1 = playfieldPosition(frame.x, frame.y); + if(options.analyze && previousFrame != null && time - frame.offset < 750){ + const position0 = playfieldPosition(previousFrame.x, previousFrame.y); + const position1 = playfieldPosition(frame.x, frame.y); - ctx.beginPath(); + ctx.beginPath(); - ctx.moveTo(...position0); - ctx.lineTo(...position1); + ctx.moveTo(...position0); + ctx.lineTo(...position1); - ctx.stroke(); - } + 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)){ + 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.strokeStyle = "white"; + ctx.strokeStyle = "white"; - const position = playfieldPosition(frame.x, frame.y); + const position = playfieldPosition(frame.x, frame.y); - ctx.beginPath(); + ctx.beginPath(); - ctx.moveTo(position[0], position[1] - 5); - ctx.lineTo(position[0], position[1] + 5); - ctx.stroke(); + ctx.moveTo(position[0], position[1] - 5); + ctx.lineTo(position[0], position[1] + 5); + ctx.stroke(); - ctx.moveTo(position[0] - 5, position[1]); - ctx.lineTo(position[0] + 5, position[1]); - ctx.stroke(); - } - } + ctx.moveTo(position[0] - 5, position[1]); + ctx.lineTo(position[0] + 5, position[1]); + ctx.stroke(); + } + } - ctx.lineWidth = 6 * scale_multiplier; - ctx.strokeStyle = "rgba(255,255,255,0.4)"; + ctx.lineWidth = 6 * scale_multiplier; + ctx.strokeStyle = "rgba(255,255,255,0.4)"; - if(frame.S == false && smokeActive){ - if(smokeActive && !options.analyze){ - ctx.stroke(); - smokeActive = false; - } - - continue; - } + if(frame.S === false && smokeActive){ + if(smokeActive && !options.analyze){ + ctx.stroke(); + smokeActive = false; + } - if(frame.S){ - if(!smokeActive){ - smokeActive = true; - ctx.beginPath(); - ctx.moveTo(...playfieldPosition(frame.x, frame.y)); - }else{ - ctx.lineTo(...playfieldPosition(frame.x, frame.y)); - } - } - } + continue; + } - if(smokeActive && !options.analyze){ - ctx.stroke(); - } + 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(replay_point){ - if(beatmap.Replay.auto !== true){ - ctx.globalAlpha = 1; + if(smokeActive && !options.analyze){ + ctx.stroke(); + } - const { K1, K2, M1, M2 } = replay_point.current; + if(replay_point){ + if(beatmap.Replay.auto !== true){ + ctx.globalAlpha = 1; - const keyOverlayTop = canvas.height / 2 - (KEY_OVERLAY_SIZE * 4 + KEY_OVERLAY_PADDING * 4) / 2; + const { K1, K2, M1, M2 } = replay_point.current; - 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); + const keyOverlayTop = canvas.height / 2 - (KEY_OVERLAY_SIZE * 4 + KEY_OVERLAY_PADDING * 4) / 2; - 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.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.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.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.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; + 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.beginPath(); - - for(const [index, previousFrame] of replay_point.previous.entries()){ - let position = playfieldPosition(previousFrame.x, previousFrame.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); + } - if(index == 0) - ctx.moveTo(...position); - else - ctx.lineTo(...position); - } + if(Array.isArray(replay_point.previous) && !options.analyze){ + ctx.globalAlpha = .35; - ctx.lineWidth = 13 * scale_multiplier; - ctx.lineCap = "round"; + ctx.beginPath(); - 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(const [index, previousFrame] of replay_point.previous.entries()){ + let position = playfieldPosition(previousFrame.x, previousFrame.y); - if(end_time){ - while(time < end_time){ - processFrame(time, options); + if(index === 0) + ctx.moveTo(...position); + else + ctx.lineTo(...position); + } - let image_data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; + ctx.lineWidth = 13 * scale_multiplier; + ctx.lineCap = "round"; - // 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); - } + 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(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){ + if (stop){ + break; + } + while (WAITING_FOR_SERVER_ACK && !stop){ + 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, + 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; + + current_frame += threads; + time += time_frame; + } + }else{ // no end time -> render single frame + processFrame(time, options); + + 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; + +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"); + }) + + 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.on('app.terminate', function(data){ + log_as_worker("Terminating..."); + process.exit(0); + }) + + stop = false; + + ipc.of.world.on( + 'receiveWork', + async function (data){ + // log_as_worker(data); + await run_worker_job(data); + ipc.of.world.emit('app.readyToTerminate', 'true'); + } + ) + + ipc.of.world.on('abort', async function(){ + stop = true; + }) + } +); + +process.on('uncaughtException', err => { + helper.error(err); + process.exit(1); }); + +process.on('message', async obj => { + await run_worker_job(obj) +}); +