e-typing chobun plus

ワードの表示・打ち切り回数保存、任意の文字間のリザルト・リプレイ再生

  1. // ==UserScript==
  2. // @name e-typing chobun plus
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.0
  5. // @description ワードの表示・打ち切り回数保存、任意の文字間のリザルト・リプレイ再生
  6. // @author tai
  7. // @license MIT
  8. // @match https://www.e-typing.ne.jp/app*
  9. // @exclude https://www.e-typing.ne.jp/app/ad*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=e-typing.ne.jp
  11. // @require https://update.gf.qytechs.cn/scripts/530545/1558131/keyboardevent-chobun.js
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // ==/UserScript==
  15.  
  16. $(document).one("loadComplete", (_, setting) => {
  17. let type = setting.querySelector("type").textContent;
  18. if (type === "4" || type === "255") {
  19. console.log("(`・▿・´ )ノ");
  20.  
  21. let snsURL = setting.querySelector("snsURL").textContent;
  22. let title = setting.querySelector("title").textContent;
  23. let mode = snsURL.includes("english") ? "E" : snsURL.includes("kana") ? "K" : "R";
  24. new Chobun(title, mode);
  25. } else {
  26. console.log("_(- ᴗ -_)_ …zzz");
  27. }
  28. })
  29.  
  30.  
  31.  
  32. class Chobun {
  33. constructor(title, mode){
  34. this.title = title;
  35. this.mode = mode;
  36.  
  37. this.chobun = JSON.parse(GM_getValue("chobun", "{}"));
  38. this.words = this.chobun[this.title]?.[this.mode]?.words ?? {};
  39. this.focusWords = [];
  40.  
  41. this.wordList = new WordList(this.title, this.mode, this.chobun, this.words, this.focusWords);
  42.  
  43. this.insertStyle();
  44.  
  45. $(document).on({
  46. "end_countdown.etyping": this.start,
  47. "replay": () => {
  48. Result.clear();
  49. Replay.clear();
  50. Typing.clear();
  51. $(document).on("end_countdown.etyping", this.start);
  52. }
  53. });
  54.  
  55. window.addEventListener("beforeunload", this.close);
  56. parent.$pp_overlay && (parent.$pp_overlay.fadeOut = (a,c,d) => {this.close(); return parent.$pp_overlay.animate({opacity:"hide"},a,c,d)});
  57. }
  58.  
  59. start = () => {
  60. let typingStartTime = performance.now();
  61. let time, char;
  62. Typing.data.push({ char: null, index: null, time: null }); //logで見やすく
  63. this.replayFlag = false;
  64.  
  65. const handleKeydown = e => {
  66. time = e.timeStamp - typingStartTime;
  67. char = this.mode === "K" ? e.kana : this.mode === "R" ? e.char.toUpperCase() : e.char;
  68. };
  69.  
  70. document.addEventListener("keydown", handleKeydown);
  71.  
  72. let index = 0;
  73. $(document).on({
  74. ["correct.etyping error.etyping"]: e => {
  75. let charData = Typing.data.at(-1);
  76.  
  77. if (e.type === "correct") {
  78. charData.index = index;
  79. charData.char = char;
  80. charData.time = time;
  81. index++;
  82. } else {
  83. charData.index = index;
  84. charData.char = char;
  85. charData.time = time;
  86. charData.isMiss = true;
  87. }
  88.  
  89. Typing.data.push({ char: null, index: null, time: null });
  90. },
  91. ["complete.etyping interrupt.etyping"]: e => {
  92. let charData = Typing.data.at(-1);
  93. charData.index = index;
  94. charData.char = char;
  95. charData.time = time;
  96. charData.isInterrupt = charData.isInterrupt = e.type === "interrupt";
  97.  
  98. document.removeEventListener("keydown", handleKeydown);
  99.  
  100. console.log(Typing.data);
  101. setTimeout(() => this.end(e.type));
  102. }
  103. })
  104.  
  105. setTimeout(() => {
  106. this.word = this.mode !== "E" ? document.getElementById("exampleText").textContent : document.getElementById("sentenceText").textContent.replace(/␣/g," ");
  107.  
  108. this.words = this.wordList.add(this.word, "show");
  109.  
  110. if (this.focusWords.length && !this.focusWords.includes(this.word)) {
  111. this.replayFlag = true;
  112. $(document).trigger("interrupt.etyping");
  113. }
  114. })
  115. }
  116.  
  117. end(type){
  118. const resultObserver = new MutationObserver(() => {
  119. if (document.getElementsByClassName("result_data").length) {
  120. if (this.replayFlag) {
  121. resultObserver.disconnect();
  122. return $(document).trigger("replay");
  123. }
  124.  
  125. if (type === "complete") {
  126. this.wordList.add(this.word, type);
  127. }
  128.  
  129. Result.init(this.mode);
  130.  
  131. resultObserver.disconnect();
  132. }
  133. })
  134.  
  135. resultObserver.observe(document.getElementById("result"), { childList: true });
  136. }
  137.  
  138. insertStyle(){
  139. document.head.insertAdjacentHTML("afterbegin",`<style>
  140. #exampleList {
  141. width: 371px !important;
  142. }
  143.  
  144. .entered {
  145. color: #ffd0a6;
  146. }
  147.  
  148. .sentence {
  149. font-size: 20px;
  150. font-family: "Consolas", "Cascadia Mono", "Menlo", "DejaVu Sans Mono", monospace;
  151. line-break: anywhere;
  152. }
  153.  
  154. .sentence span {
  155. cursor: pointer;
  156. }
  157.  
  158. .sentence .hover {
  159. outline: 1px solid #000000;
  160. }
  161.  
  162. .sentence .selected {
  163. background-color: rgba(5, 127, 255, 0.8);
  164. outline: 1px solid #0000ff;
  165. }
  166.  
  167. .result_data.fixed {
  168. background-color: rgba(255, 255, 0, 0.5) !important;
  169. }
  170. </style>`);
  171. }
  172.  
  173. close(){
  174. parent.document.getElementById("word_list").remove();
  175. }
  176. }
  177.  
  178.  
  179.  
  180. class Result {
  181. static init(mode){
  182. this.mode = mode;
  183.  
  184. this.sentence = document.getElementsByClassName("sentence")[0];
  185. !document.getElementById("latency") && this.plus(Typing.data);
  186.  
  187. this.prev = document.getElementById("prev");
  188. this.savePrev = this.prev.innerHTML;
  189.  
  190. this.sentence.title = "クリックでこの文字を固定(もう一度押して解除)\n\nショートカット:\n[s] リザルトを固定 (もう一度押して解除)\n[a] リプレイ再生\n[Escape] リプレイ停止、リザルト画面初期化";
  191. [...this.sentence.children].forEach(e => e.textContent = e.textContent === " " ? "_" : e.textContent);
  192. this.mode === "K" && (this.sentence.style.fontSize = "16px");
  193.  
  194. Replay.init();
  195.  
  196. this.fixed = false;
  197. this.selected = null;
  198. if (Typing.latestIndex()) {
  199. this.sentence.addEventListener("click", e => e.target.matches(".sentence span") && !this.fixed && this.#sentenceClick(e));
  200. this.sentence.addEventListener("mouseover", e => e.target.matches(".sentence span") && !this.fixed && this.#sentenceMouseOver(e));
  201. this.sentence.addEventListener("mouseleave", e => !this.fixed && this.#sentenceMouseLeave(e));
  202.  
  203. document.addEventListener("keydown", this.#handleKeydown);
  204. parent.document.addEventListener("keydown", this.#handleKeydown);
  205. }
  206. }
  207.  
  208. static plus(typingData){
  209. document.getElementById("app").style.height = "502px";
  210. document.querySelector("#result article").style.height = "452px";
  211. document.getElementById("current").style.height = "367px";
  212. document.getElementById("prev").style.height = "367px";
  213. document.getElementById("exampleList").style.height = "284px";
  214. document.querySelectorAll(".result_data").forEach(e => { e.children[0].children[7].remove(); e.style.height = "318px" });
  215.  
  216.  
  217. document.getElementsByClassName("result_data")[1].children[0].insertAdjacentHTML("beforeend", `<li id="previous_latency"><div class="data">${this.latency === undefined ? "-" : (this.latency / 1000).toFixed(3)}</div></li><li id="previous_rkpm"><div class="data">${this.rkpm === undefined ? "-" : this.rkpm.toFixed(2)}</div></li>`);
  218.  
  219. this.latency = Typing.latency();
  220. this.rkpm = Typing.rkpm();
  221. document.getElementsByClassName("result_data")[0].children[0].insertAdjacentHTML("beforeend", `<li id="latency"><div class="title">Latency</div><div class="data">${(this.latency / 1000).toFixed(3)}</div></li><li id="rkpm"><div class="title">RKPM</div><div class="data">${this.rkpm.toFixed(2)}</div></li>`);
  222.  
  223.  
  224. this.sentence.innerHTML = this.sentence.textContent.split("").map((char, i) => {
  225. let charData = Typing.data.findLast(e => e.index === i);
  226. let isMiss = Typing.data.some(e => e.index === i && e.isMiss);
  227.  
  228. return !charData || charData.isInterrupt ? `<span style="opacity: 0.6; display: inline;">${char}</span>` : isMiss ? `<span class="miss">${char}</span>` : `<span>${char}</span>`;
  229. }).join("");
  230. }
  231.  
  232. static show(start, end, indexBreak = true){
  233. start = Math.max(0, start);
  234. end = indexBreak ? Math.min(Typing.latestIndex() - (Typing.data.at(-1).isInterrupt ? 1 : 0), end) : end;
  235.  
  236. const data = Typing.result(start, end, indexBreak);
  237.  
  238. document.querySelector("#prev h1").textContent = indexBreak ? `${start + 1}~${end + 1}まで` : `${data.typingCount}${data.missTypeCount ? " (" + data.missTypeCount + ")文字" : "文字"}`;
  239. let prevRsltElem = document.getElementsByClassName("result_data")[1].getElementsByClassName("data");
  240. prevRsltElem[0].textContent = data.score.toFixed(2);
  241. prevRsltElem[1].textContent = data.level;
  242. prevRsltElem[2].textContent = data.inputTime;
  243. prevRsltElem[3].textContent = data.typingCount;
  244. prevRsltElem[4].textContent = data.missTypeCount;
  245. prevRsltElem[5].textContent = data.wpm.toFixed(2);
  246. prevRsltElem[6].textContent = (data.correctRate / 100).toFixed(2) + "%";
  247. prevRsltElem[7].textContent = (data.latency / 1000).toFixed(3);
  248. prevRsltElem[8].textContent = data.rkpm.toFixed(2);
  249. }
  250.  
  251. static #sentenceClick = e => {
  252. let sentences = [...e.target.parentNode.children];
  253.  
  254. if (this.selected === null) {
  255. this.selected = Math.min(Typing.latestIndex() - (Typing.data.at(-1).isInterrupt ? 1 : 0), sentences.indexOf(e.target));
  256. this.show(this.selected, this.selected);
  257. sentences[this.selected].classList.add("selected");
  258. } else {
  259. this.selected = null;
  260. this.show(0, sentences.indexOf(e.target));
  261. document.getElementsByClassName("selected")[0]?.classList.remove("selected");
  262. }
  263. }
  264.  
  265. static #sentenceMouseOver = e => {
  266. let sentences = [...e.target.parentNode.children];
  267. let targetIndex = sentences.indexOf(e.target);
  268.  
  269. document.getElementsByClassName("hover")[0]?.classList.remove("hover");
  270. sentences[Math.min(Typing.latestIndex() - (Typing.data.at(-1).isInterrupt ? 1 : 0), targetIndex)].classList.add("hover");
  271.  
  272. let [start, end] = [this.selected || 0, targetIndex].sort((a, b) => a - b);
  273. this.show(start, end);
  274. }
  275.  
  276. static #sentenceMouseLeave = e => {
  277. if (e.relatedTarget?.className !== "time-tooltip" && e.relatedTarget?.parentElement.className !== "time-tooltip") {
  278. this.selected = null;
  279. document.getElementsByClassName("hover")[0]?.classList.remove("hover");
  280. document.getElementsByClassName("selected")[0]?.classList.remove("selected");
  281.  
  282. this.prev.innerHTML = this.savePrev;
  283. }
  284. }
  285.  
  286. static #handleKeydown = e => {
  287. switch (e.key) {
  288. case "s":
  289. this.fixed && this.selected && (this.selected = null, document.getElementsByClassName("selected")[0]?.classList.remove("selected"));
  290. this.fixed && document.getElementsByClassName("hover")[0]?.classList.remove("hover");
  291.  
  292.  
  293. this.fixed = !this.fixed;
  294. document.getElementsByClassName("result_data")[1].classList.toggle("fixed");
  295. break;
  296. }
  297. }
  298.  
  299. static clear(){
  300. this.prev = null;
  301. this.savePrev = null;
  302.  
  303. document.removeEventListener("keydown", this.#handleKeydown);
  304. parent.document.removeEventListener("keydown", this.#handleKeydown);
  305. }
  306. }
  307.  
  308. class WordList {
  309. constructor(title, mode, chobun, words, focusWords){
  310. this.title = title;
  311. this.mode = mode;
  312.  
  313. this.chobun = chobun;
  314. this.words = words;
  315. this.focusWords = focusWords;
  316.  
  317. this.pDoc = parent.document;
  318. this.insert();
  319. }
  320.  
  321. insert(){
  322. let top = parent.scrollY + 137.5;
  323. let left = this.pDoc.documentElement.clientWidth / 2 - 374;
  324.  
  325. this.pDoc.body.insertAdjacentHTML("afterbegin",`
  326. <table id="word_list" style="top: ${top + 371 + 90}px; left: ${left + 10 + 57.5 + 608 * 3 / 4}px;">
  327. <tbody id="words"></tbody>
  328. </table>`);
  329.  
  330. this.pDoc.head.insertAdjacentHTML("afterbegin",`
  331. <style>
  332. #word_list {
  333. position: absolute;
  334. z-index: 15000;
  335. color: black;
  336. padding: 5px;
  337. background-color: rgba(5, 127, 255, 0.8);
  338. outline: 1px solid #0000ff;
  339. box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
  340. border-radius: 10px;
  341. border-collapse: separate;
  342. user-select: none;
  343. }
  344.  
  345. #word_list:hover {
  346. cursor: grab;
  347. }
  348.  
  349. #word_list:active {
  350. cursor: grabbing;
  351. }
  352.  
  353. #words td {
  354. color: black;
  355. max-width: 300px;
  356. height: 20px;
  357. line-height: 2;
  358. padding-left: 5px;
  359. white-space: nowrap;
  360. overflow: hidden;
  361. text-overflow: ellipsis;
  362. }
  363.  
  364. .focus_word {
  365. outline: 1px solid aqua;
  366. background-color: rgba(127, 255, 212, 0.5);
  367. border-radius: 10px;
  368. }
  369.  
  370. .highlight::after {
  371. content: "";
  372. position: absolute;
  373. animation: pathmove 3s ease-in-out infinite;
  374. opacity: 0;
  375. box-shadow: 0px -3px 1px 1px rgba(222, 222, 7, 0.9);
  376. }
  377.  
  378. @keyframes pathmove {
  379. 0% { width: 0; left: 0; opacity: 0; }
  380. 10% { opacity: 1; }
  381. 30% { width: 200px; }
  382. 70% { left: 60%; opacity: 1; }
  383. 100% { width: 30px; left: 60%; opacity: 0; }
  384. }
  385. `);
  386.  
  387.  
  388.  
  389. this.wordList = this.pDoc.getElementById("word_list");
  390.  
  391. this.wordList.addEventListener("pointermove", function(e){
  392. if (e.buttons) {
  393. this.style.left = this.offsetLeft + e.movementX + "px";
  394. this.style.top = this.offsetTop + e.movementY + "px";
  395. this.setPointerCapture(e.pointerId);
  396. }
  397. });
  398.  
  399. this.wordList.addEventListener("click", e => {
  400. if (e.target.matches("td") && [...e.target.classList].includes("word")) {
  401. let targetWord = e.target.textContent;
  402. if (!this.focusWords.includes(targetWord)) {
  403. this.focusWords.push(targetWord);
  404. e.target.classList.add("focus_word");
  405. } else {
  406. this.focusWords.splice(this.focusWords.findIndex(word => word === targetWord), 1);
  407. e.target.classList.remove("focus_word");
  408. }
  409. }
  410. })
  411.  
  412. this.show(Object.keys(this.words));
  413. }
  414.  
  415. add(word, type){
  416. this.words[word] = this.words[word] || { count: 0, compCount: 0 };
  417. this.words[word].count += type === "show" ? 1 : 0;
  418. this.words[word].compCount += type === "complete" ? 1 : 0;
  419.  
  420. this.chobun[this.title] ??= {};
  421. this.chobun[this.title][this.mode] = { words: this.words };
  422. GM_setValue("chobun", JSON.stringify(this.chobun));
  423.  
  424. this.show([word]);
  425. }
  426.  
  427. show(addedWords){
  428. let innerHTML = Object.keys(this.words).sort((a, b) => this.words[b].count - this.words[a].count).reduce((accHTML, word) => {
  429. let count = this.words[word].count;
  430. let compCount = this.words[word].compCount;
  431. let compRate = (compCount / count * 100).toFixed(2);
  432. let isFocusWord = this.focusWords.includes(word);
  433.  
  434. return accHTML + `<tr>
  435. <td title="${compRate}%">${compCount}/${count}</td>
  436. <td title="${word}" class="word${isFocusWord ? " focus_word" : ""}">${word}</td>
  437. </tr>`;
  438. }, "") || "<td>Let's typing!</td>";
  439.  
  440. this.pDoc.getElementById("words").innerHTML = innerHTML;
  441. this.highlight(addedWords);
  442. }
  443.  
  444. highlight(addedWords){
  445. addedWords.forEach(addedWord => {
  446. let target = this.wordList.querySelector(`[title="${addedWord}"]`);
  447. target.insertAdjacentHTML("beforeend", "<div class='highlight'></div>");
  448. })
  449.  
  450. setTimeout(() => { [...this.wordList.getElementsByClassName("highlight")].forEach(e => e.remove()); }, 3000);
  451. }
  452. }
  453.  
  454. class Replay {
  455. static scrollLine = 7;
  456.  
  457. static init(){
  458. document.getElementById("btn_area").insertAdjacentHTML("beforeend",`<a id="miss_only_btn" class="btn">リプレイ</a>`);
  459. document.getElementById("miss_only_btn").addEventListener("click", () => Replay.load(...[Result.selected || 0, !document.getElementsByClassName("hover")[0] ? Typing.latestIndex() : [...document.getElementsByClassName("sentence")[0].children].indexOf(document.getElementsByClassName("hover")[0])].sort((a, b) => a - b), true));
  460.  
  461. document.addEventListener("keydown", this.#handleKeydown);
  462. parent.document.addEventListener("keydown", this.#handleKeydown);
  463. }
  464.  
  465. static load(start, end, play = true){
  466. this.data = Typing.dataSlice(start, end, true);
  467.  
  468. this.sentence = document.getElementsByClassName("sentence")[0];
  469. this.sentences = document.querySelectorAll(".sentence span");
  470.  
  471. this.charWidth = this.sentences[0].getBoundingClientRect().width;
  472. this.charHeight = this.sentences[0].getBoundingClientRect().height;
  473. this.lineLimit = Math.floor(this.sentence.getBoundingClientRect().width / this.charWidth);
  474.  
  475.  
  476. play && this.play(start, end);
  477. }
  478.  
  479. static play(start, end){
  480. document.querySelector("#prev h1").textContent = "-";
  481. document.getElementsByClassName("result_data")[1].querySelectorAll(".data").forEach(e => e.textContent = "-");
  482. this.sentences.forEach((e, i) => (i < start || i > end) && (e.style = "opacity: 0.6; display: inline;"));
  483. this.sentences.forEach((_, i) => this.sentences[i].classList.remove("miss", "entered"));
  484.  
  485. Result.fixed = true;
  486. document.getElementsByClassName("result_data")[1].classList.add("fixed");
  487.  
  488. document.getElementById("exampleList").scrollTo({ top: this.sentences[start].offsetTop });
  489.  
  490. this.stop = false;
  491. let startIndex = Typing.data.findIndex(e => e.index === start);
  492. let i = 0;
  493. let startTime = performance.now();
  494. this.tick(() => {
  495. if (!this.data?.[i] || !document.getElementsByClassName("sentence")[0]) {
  496. return false;
  497. }
  498.  
  499. let currentTime = performance.now() - startTime;
  500. let charTime = this.data[i].time;
  501.  
  502. if (currentTime >= charTime) {
  503. if (this.data[i].isInterrupt) {
  504. return false;
  505. }
  506.  
  507. let char = this.data[i].char;
  508. let isMiss = this.data[i].isMiss;
  509. let index = this.data[i].index;
  510.  
  511. this.sentences[index].textContent = char;
  512. this.sentences[index].classList.add(isMiss ? "miss" : "entered");
  513. Result.show(startIndex, startIndex + i, false);
  514. i++;
  515.  
  516. if (!isMiss && this.lineLimit * (this.scrollLine - Number(!!(start % this.lineLimit))) <= index - start && !(index % this.lineLimit)) {
  517. document.getElementById("exampleList").scrollBy(0, this.charHeight);
  518. }
  519. }
  520.  
  521. return true;
  522. })
  523. }
  524.  
  525. static tick(callback) {
  526. if (this.currentTick) {
  527. cancelAnimationFrame(this.currentTick);
  528. console.log("stop");
  529. }
  530.  
  531. const loop = () => {
  532. if (!callback() || this.stop) {
  533. console.log(this.stop ? "stop" : "end");
  534. this.data = null;
  535. this.currentTick = null;
  536.  
  537. Typing.data.forEach(e => {
  538. if (!e.isInterrupt) {
  539. this.sentences[e.index].style = "";
  540. e.isMiss && this.sentences[e.index].classList.add("miss");
  541. }
  542. })
  543. return;
  544. }
  545. this.currentTick = requestAnimationFrame(loop);
  546. };
  547.  
  548. this.currentTick = requestAnimationFrame(loop);
  549. }
  550.  
  551. static #handleKeydown = e => {
  552. switch (e.key) {
  553. case "a":
  554. document.getElementById("miss_only_btn").click();
  555. break;
  556. case "Escape":
  557. this.stop = true;
  558.  
  559. this.sentences && Typing.data.forEach(e => {
  560. !e.isMiss && !e.isInterrupt && (this.sentences[e.index].textContent = e.char);
  561. e.isMiss && this.sentences[e.index].classList.add("miss");
  562. this.sentences[e.index].style = e.isInterrupt ? "opacity: 0.6; display: inline;" : "";
  563. this.sentences[e.index].classList.remove("entered");
  564. })
  565.  
  566. Result.selected && (Result.selected = null, document.getElementsByClassName("selected")[0]?.classList.remove("selected"));
  567. document.getElementsByClassName("hover")[0]?.classList.remove("hover");
  568.  
  569. Result.fixed = false;
  570. document.getElementsByClassName("result_data")[1].classList.remove("fixed");
  571. setTimeout(() => Result.prev.innerHTML = Result.savePrev);
  572. break;
  573. }
  574. }
  575.  
  576. static clear(){
  577. this.stop = false;
  578. this.data = null;
  579. this.sentence = null;
  580. this.sentences = null;
  581.  
  582. document.removeEventListener("keydown", this.#handleKeydown);
  583. parent.document.removeEventListener("keydown", this.#handleKeydown);
  584. }
  585. }
  586.  
  587.  
  588.  
  589. class Typing {
  590. static levelList = ["E-", "E", "E+", "D-", "D", "D+", "C-", "C", "C+", "B-", "B", "B+", "A-", "A", "A+", "S", "Good!", "Fast", "Thunder", "Ninja", "Comet", "Professor", "LaserBeam", "EddieVH", "Meijin", "Rocket", "Tatujin", "Jedi", "Godhand", "Joker", "Error"];
  591. static data = [];
  592.  
  593. static score(data = this.data){
  594. const ms = this.data.at(-1).time;
  595. const typingCount = this.typingCount(data);
  596. const missTypeCount = this.missTypeCount(data);
  597. const correctRate = Math.floor(Math.max(10000 * (typingCount - missTypeCount) / typingCount, 0));
  598. return 60000 * (typingCount - missTypeCount) / ms * (correctRate / 10000) ** 2;
  599. }
  600.  
  601. static level(score){
  602. return this.levelList[score < 22 ? 0 : score < 39 ? 1 : score < 56 ? 2 : score < 73 ? 3 : score < 90 ? 4 : score < 107 ? 5 : score < 124 ? 6 : score < 141 ? 7 : score < 158 ? 8 : score < 175 ? 9 : score < 192 ? 10 : score < 209 ? 11 : score < 226 ? 12 : score < 243 ? 13 : score < 260 ? 14 : score < 277 ? 15 : score < 300 ? 16 : score < 325 ? 17 : score < 350 ? 18 : score < 375 ? 19 : score < 400 ? 20 : score < 450 ? 21 : score < 500 ? 22 : score < 550 ? 23 : score < 600 ? 24 : score < 650 ? 25 : score < 700 ? 26 : score < 750 ? 27 : score < 800 ? 28 : score < 1100 ? 29 : 30];
  603. }
  604.  
  605. static inputTime(data = this.data){
  606. const ms = data.at(-1).time;
  607. return (ms < 60000 ? "" : Math.floor(ms / 60000) + "分") + (ms / 1000 % 60).toFixed(2).replace(".","秒");
  608. }
  609.  
  610. static typingCount(data = this.data){
  611. return data.filter(e => !e.isMiss && !e.isInterrupt).length;
  612. }
  613.  
  614. static missTypeCount(data = this.data){
  615. return data.filter(e => e.isMiss && !e.isInterrupt).length;
  616. }
  617.  
  618. static wpm(data = this.data){
  619. const ms = this.data.at(-1).time;
  620. const typingCount = this.typingCount(data);
  621. return Math.floor(typingCount * (6000000 / ms)) / 100;
  622. }
  623.  
  624. static correctRate(data = this.data){
  625. const typingCount = this.typingCount(data);
  626. const missTypeCount = this.missTypeCount(data);
  627. return Math.floor(Math.max(10000 * (typingCount - missTypeCount) / typingCount, 0));
  628. }
  629.  
  630. static latency(data = this.data){
  631. return data.find(e => !e.isMiss && !e.isInterrupt)?.time || NaN;
  632. }
  633.  
  634. static rkpm(data = this.data){
  635. const ms = this.data.at(-1).time;
  636. const typingCount = this.typingCount(data);
  637. const latency = this.latency(data);
  638. return (typingCount - 1) / (ms - latency) * 60000 || 0;
  639. }
  640.  
  641. static result(start = 0, end = this.data.length, indexBreak){
  642. const data = this.dataSlice(start, end, indexBreak);
  643.  
  644. const latency = this.latency(data);
  645. const ms = data.at(-1).time;
  646. const inputTime = (ms < 60000 ? "" : Math.floor(ms / 60000) + "分") + (ms / 1000 % 60).toFixed(2).replace(".","秒");
  647.  
  648. const typingCount = this.typingCount(data);
  649. const wpm = Math.floor(typingCount * (6000000 / ms)) / 100;
  650.  
  651. const missTypeCount = this.missTypeCount(data);
  652. const correctRate = Math.floor(Math.max(10000 * (typingCount - missTypeCount) / typingCount, 0));
  653. const score = 60000 * (typingCount - missTypeCount) / ms * (correctRate / 10000) ** 2;
  654. const level = this.level(score);
  655.  
  656. const rkpm = (typingCount - 1) / (ms - latency) * 60000 || 0;
  657.  
  658. return {
  659. score: score,
  660. level: level,
  661. inputTime: inputTime,
  662. typingCount: typingCount,
  663. missTypeCount: missTypeCount,
  664. wpm: wpm,
  665. correctRate: correctRate,
  666. latency: latency,
  667. rkpm: rkpm
  668. }
  669. }
  670.  
  671. static dataSlice(start, end, indexBreak){
  672. let data = indexBreak ? this.data.slice(this.data.findIndex(e => e.index === start), this.data.findLastIndex(e => e.index === Math.min(end, this.latestIndex())) + 1) : this.data.slice(start, end + 1);
  673. return start === 0 ? data : data.map(e => ({ ...e, time: e.time - this.data.findLast(e => e.index === (!indexBreak ? this.data[start].index : start) - 1).time }));
  674. }
  675.  
  676. static latestIndex(){
  677. return this.data.at(-1).index;
  678. }
  679.  
  680. static clear(){
  681. this.data = [];
  682. }
  683. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址