您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
ワードの表示・打ち切り回数保存、任意の文字間のリザルト・リプレイ再生
// ==UserScript== // @name e-typing chobun plus // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description ワードの表示・打ち切り回数保存、任意の文字間のリザルト・リプレイ再生 // @author tai // @license MIT // @match https://www.e-typing.ne.jp/app* // @exclude https://www.e-typing.ne.jp/app/ad* // @icon https://www.google.com/s2/favicons?sz=64&domain=e-typing.ne.jp // @require https://update.gf.qytechs.cn/scripts/530545/1558131/keyboardevent-chobun.js // @grant GM_setValue // @grant GM_getValue // ==/UserScript== $(document).one("loadComplete", (_, setting) => { let type = setting.querySelector("type").textContent; if (type === "4" || type === "255") { console.log("(`・▿・´ )ノ"); let snsURL = setting.querySelector("snsURL").textContent; let title = setting.querySelector("title").textContent; let mode = snsURL.includes("english") ? "E" : snsURL.includes("kana") ? "K" : "R"; new Chobun(title, mode); } else { console.log("_(- ᴗ -_)_ …zzz"); } }) class Chobun { constructor(title, mode){ this.title = title; this.mode = mode; this.chobun = JSON.parse(GM_getValue("chobun", "{}")); this.words = this.chobun[this.title]?.[this.mode]?.words ?? {}; this.focusWords = []; this.wordList = new WordList(this.title, this.mode, this.chobun, this.words, this.focusWords); this.insertStyle(); $(document).on({ "end_countdown.etyping": this.start, "replay": () => { Result.clear(); Replay.clear(); Typing.clear(); $(document).on("end_countdown.etyping", this.start); } }); window.addEventListener("beforeunload", this.close); parent.$pp_overlay && (parent.$pp_overlay.fadeOut = (a,c,d) => {this.close(); return parent.$pp_overlay.animate({opacity:"hide"},a,c,d)}); } start = () => { let typingStartTime = performance.now(); let time, char; Typing.data.push({ char: null, index: null, time: null }); //logで見やすく this.replayFlag = false; const handleKeydown = e => { time = e.timeStamp - typingStartTime; char = this.mode === "K" ? e.kana : this.mode === "R" ? e.char.toUpperCase() : e.char; }; document.addEventListener("keydown", handleKeydown); let index = 0; $(document).on({ ["correct.etyping error.etyping"]: e => { let charData = Typing.data.at(-1); if (e.type === "correct") { charData.index = index; charData.char = char; charData.time = time; index++; } else { charData.index = index; charData.char = char; charData.time = time; charData.isMiss = true; } Typing.data.push({ char: null, index: null, time: null }); }, ["complete.etyping interrupt.etyping"]: e => { let charData = Typing.data.at(-1); charData.index = index; charData.char = char; charData.time = time; charData.isInterrupt = charData.isInterrupt = e.type === "interrupt"; document.removeEventListener("keydown", handleKeydown); console.log(Typing.data); setTimeout(() => this.end(e.type)); } }) setTimeout(() => { this.word = this.mode !== "E" ? document.getElementById("exampleText").textContent : document.getElementById("sentenceText").textContent.replace(/␣/g," "); this.words = this.wordList.add(this.word, "show"); if (this.focusWords.length && !this.focusWords.includes(this.word)) { this.replayFlag = true; $(document).trigger("interrupt.etyping"); } }) } end(type){ const resultObserver = new MutationObserver(() => { if (document.getElementsByClassName("result_data").length) { if (this.replayFlag) { resultObserver.disconnect(); return $(document).trigger("replay"); } if (type === "complete") { this.wordList.add(this.word, type); } Result.init(this.mode); resultObserver.disconnect(); } }) resultObserver.observe(document.getElementById("result"), { childList: true }); } insertStyle(){ document.head.insertAdjacentHTML("afterbegin",`<style> #exampleList { width: 371px !important; } .entered { color: #ffd0a6; } .sentence { font-size: 20px; font-family: "Consolas", "Cascadia Mono", "Menlo", "DejaVu Sans Mono", monospace; line-break: anywhere; } .sentence span { cursor: pointer; } .sentence .hover { outline: 1px solid #000000; } .sentence .selected { background-color: rgba(5, 127, 255, 0.8); outline: 1px solid #0000ff; } .result_data.fixed { background-color: rgba(255, 255, 0, 0.5) !important; } </style>`); } close(){ parent.document.getElementById("word_list").remove(); } } class Result { static init(mode){ this.mode = mode; this.sentence = document.getElementsByClassName("sentence")[0]; !document.getElementById("latency") && this.plus(Typing.data); this.prev = document.getElementById("prev"); this.savePrev = this.prev.innerHTML; this.sentence.title = "クリックでこの文字を固定(もう一度押して解除)\n\nショートカット:\n[s] リザルトを固定 (もう一度押して解除)\n[a] リプレイ再生\n[Escape] リプレイ停止、リザルト画面初期化"; [...this.sentence.children].forEach(e => e.textContent = e.textContent === " " ? "_" : e.textContent); this.mode === "K" && (this.sentence.style.fontSize = "16px"); Replay.init(); this.fixed = false; this.selected = null; if (Typing.latestIndex()) { this.sentence.addEventListener("click", e => e.target.matches(".sentence span") && !this.fixed && this.#sentenceClick(e)); this.sentence.addEventListener("mouseover", e => e.target.matches(".sentence span") && !this.fixed && this.#sentenceMouseOver(e)); this.sentence.addEventListener("mouseleave", e => !this.fixed && this.#sentenceMouseLeave(e)); document.addEventListener("keydown", this.#handleKeydown); parent.document.addEventListener("keydown", this.#handleKeydown); } } static plus(typingData){ document.getElementById("app").style.height = "502px"; document.querySelector("#result article").style.height = "452px"; document.getElementById("current").style.height = "367px"; document.getElementById("prev").style.height = "367px"; document.getElementById("exampleList").style.height = "284px"; document.querySelectorAll(".result_data").forEach(e => { e.children[0].children[7].remove(); e.style.height = "318px" }); 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>`); this.latency = Typing.latency(); this.rkpm = Typing.rkpm(); 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>`); this.sentence.innerHTML = this.sentence.textContent.split("").map((char, i) => { let charData = Typing.data.findLast(e => e.index === i); let isMiss = Typing.data.some(e => e.index === i && e.isMiss); return !charData || charData.isInterrupt ? `<span style="opacity: 0.6; display: inline;">${char}</span>` : isMiss ? `<span class="miss">${char}</span>` : `<span>${char}</span>`; }).join(""); } static show(start, end, indexBreak = true){ start = Math.max(0, start); end = indexBreak ? Math.min(Typing.latestIndex() - (Typing.data.at(-1).isInterrupt ? 1 : 0), end) : end; const data = Typing.result(start, end, indexBreak); document.querySelector("#prev h1").textContent = indexBreak ? `${start + 1}~${end + 1}まで` : `${data.typingCount}${data.missTypeCount ? " (" + data.missTypeCount + ")文字" : "文字"}`; let prevRsltElem = document.getElementsByClassName("result_data")[1].getElementsByClassName("data"); prevRsltElem[0].textContent = data.score.toFixed(2); prevRsltElem[1].textContent = data.level; prevRsltElem[2].textContent = data.inputTime; prevRsltElem[3].textContent = data.typingCount; prevRsltElem[4].textContent = data.missTypeCount; prevRsltElem[5].textContent = data.wpm.toFixed(2); prevRsltElem[6].textContent = (data.correctRate / 100).toFixed(2) + "%"; prevRsltElem[7].textContent = (data.latency / 1000).toFixed(3); prevRsltElem[8].textContent = data.rkpm.toFixed(2); } static #sentenceClick = e => { let sentences = [...e.target.parentNode.children]; if (this.selected === null) { this.selected = Math.min(Typing.latestIndex() - (Typing.data.at(-1).isInterrupt ? 1 : 0), sentences.indexOf(e.target)); this.show(this.selected, this.selected); sentences[this.selected].classList.add("selected"); } else { this.selected = null; this.show(0, sentences.indexOf(e.target)); document.getElementsByClassName("selected")[0]?.classList.remove("selected"); } } static #sentenceMouseOver = e => { let sentences = [...e.target.parentNode.children]; let targetIndex = sentences.indexOf(e.target); document.getElementsByClassName("hover")[0]?.classList.remove("hover"); sentences[Math.min(Typing.latestIndex() - (Typing.data.at(-1).isInterrupt ? 1 : 0), targetIndex)].classList.add("hover"); let [start, end] = [this.selected || 0, targetIndex].sort((a, b) => a - b); this.show(start, end); } static #sentenceMouseLeave = e => { if (e.relatedTarget?.className !== "time-tooltip" && e.relatedTarget?.parentElement.className !== "time-tooltip") { this.selected = null; document.getElementsByClassName("hover")[0]?.classList.remove("hover"); document.getElementsByClassName("selected")[0]?.classList.remove("selected"); this.prev.innerHTML = this.savePrev; } } static #handleKeydown = e => { switch (e.key) { case "s": this.fixed && this.selected && (this.selected = null, document.getElementsByClassName("selected")[0]?.classList.remove("selected")); this.fixed && document.getElementsByClassName("hover")[0]?.classList.remove("hover"); this.fixed = !this.fixed; document.getElementsByClassName("result_data")[1].classList.toggle("fixed"); break; } } static clear(){ this.prev = null; this.savePrev = null; document.removeEventListener("keydown", this.#handleKeydown); parent.document.removeEventListener("keydown", this.#handleKeydown); } } class WordList { constructor(title, mode, chobun, words, focusWords){ this.title = title; this.mode = mode; this.chobun = chobun; this.words = words; this.focusWords = focusWords; this.pDoc = parent.document; this.insert(); } insert(){ let top = parent.scrollY + 137.5; let left = this.pDoc.documentElement.clientWidth / 2 - 374; this.pDoc.body.insertAdjacentHTML("afterbegin",` <table id="word_list" style="top: ${top + 371 + 90}px; left: ${left + 10 + 57.5 + 608 * 3 / 4}px;"> <tbody id="words"></tbody> </table>`); this.pDoc.head.insertAdjacentHTML("afterbegin",` <style> #word_list { position: absolute; z-index: 15000; color: black; padding: 5px; background-color: rgba(5, 127, 255, 0.8); outline: 1px solid #0000ff; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); border-radius: 10px; border-collapse: separate; user-select: none; } #word_list:hover { cursor: grab; } #word_list:active { cursor: grabbing; } #words td { color: black; max-width: 300px; height: 20px; line-height: 2; padding-left: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .focus_word { outline: 1px solid aqua; background-color: rgba(127, 255, 212, 0.5); border-radius: 10px; } .highlight::after { content: ""; position: absolute; animation: pathmove 3s ease-in-out infinite; opacity: 0; box-shadow: 0px -3px 1px 1px rgba(222, 222, 7, 0.9); } @keyframes pathmove { 0% { width: 0; left: 0; opacity: 0; } 10% { opacity: 1; } 30% { width: 200px; } 70% { left: 60%; opacity: 1; } 100% { width: 30px; left: 60%; opacity: 0; } } `); this.wordList = this.pDoc.getElementById("word_list"); this.wordList.addEventListener("pointermove", function(e){ if (e.buttons) { this.style.left = this.offsetLeft + e.movementX + "px"; this.style.top = this.offsetTop + e.movementY + "px"; this.setPointerCapture(e.pointerId); } }); this.wordList.addEventListener("click", e => { if (e.target.matches("td") && [...e.target.classList].includes("word")) { let targetWord = e.target.textContent; if (!this.focusWords.includes(targetWord)) { this.focusWords.push(targetWord); e.target.classList.add("focus_word"); } else { this.focusWords.splice(this.focusWords.findIndex(word => word === targetWord), 1); e.target.classList.remove("focus_word"); } } }) this.show(Object.keys(this.words)); } add(word, type){ this.words[word] = this.words[word] || { count: 0, compCount: 0 }; this.words[word].count += type === "show" ? 1 : 0; this.words[word].compCount += type === "complete" ? 1 : 0; this.chobun[this.title] ??= {}; this.chobun[this.title][this.mode] = { words: this.words }; GM_setValue("chobun", JSON.stringify(this.chobun)); this.show([word]); } show(addedWords){ let innerHTML = Object.keys(this.words).sort((a, b) => this.words[b].count - this.words[a].count).reduce((accHTML, word) => { let count = this.words[word].count; let compCount = this.words[word].compCount; let compRate = (compCount / count * 100).toFixed(2); let isFocusWord = this.focusWords.includes(word); return accHTML + `<tr> <td title="${compRate}%">${compCount}/${count}</td> <td title="${word}" class="word${isFocusWord ? " focus_word" : ""}">${word}</td> </tr>`; }, "") || "<td>Let's typing!</td>"; this.pDoc.getElementById("words").innerHTML = innerHTML; this.highlight(addedWords); } highlight(addedWords){ addedWords.forEach(addedWord => { let target = this.wordList.querySelector(`[title="${addedWord}"]`); target.insertAdjacentHTML("beforeend", "<div class='highlight'></div>"); }) setTimeout(() => { [...this.wordList.getElementsByClassName("highlight")].forEach(e => e.remove()); }, 3000); } } class Replay { static scrollLine = 7; static init(){ document.getElementById("btn_area").insertAdjacentHTML("beforeend",`<a id="miss_only_btn" class="btn">リプレイ</a>`); 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)); document.addEventListener("keydown", this.#handleKeydown); parent.document.addEventListener("keydown", this.#handleKeydown); } static load(start, end, play = true){ this.data = Typing.dataSlice(start, end, true); this.sentence = document.getElementsByClassName("sentence")[0]; this.sentences = document.querySelectorAll(".sentence span"); this.charWidth = this.sentences[0].getBoundingClientRect().width; this.charHeight = this.sentences[0].getBoundingClientRect().height; this.lineLimit = Math.floor(this.sentence.getBoundingClientRect().width / this.charWidth); play && this.play(start, end); } static play(start, end){ document.querySelector("#prev h1").textContent = "-"; document.getElementsByClassName("result_data")[1].querySelectorAll(".data").forEach(e => e.textContent = "-"); this.sentences.forEach((e, i) => (i < start || i > end) && (e.style = "opacity: 0.6; display: inline;")); this.sentences.forEach((_, i) => this.sentences[i].classList.remove("miss", "entered")); Result.fixed = true; document.getElementsByClassName("result_data")[1].classList.add("fixed"); document.getElementById("exampleList").scrollTo({ top: this.sentences[start].offsetTop }); this.stop = false; let startIndex = Typing.data.findIndex(e => e.index === start); let i = 0; let startTime = performance.now(); this.tick(() => { if (!this.data?.[i] || !document.getElementsByClassName("sentence")[0]) { return false; } let currentTime = performance.now() - startTime; let charTime = this.data[i].time; if (currentTime >= charTime) { if (this.data[i].isInterrupt) { return false; } let char = this.data[i].char; let isMiss = this.data[i].isMiss; let index = this.data[i].index; this.sentences[index].textContent = char; this.sentences[index].classList.add(isMiss ? "miss" : "entered"); Result.show(startIndex, startIndex + i, false); i++; if (!isMiss && this.lineLimit * (this.scrollLine - Number(!!(start % this.lineLimit))) <= index - start && !(index % this.lineLimit)) { document.getElementById("exampleList").scrollBy(0, this.charHeight); } } return true; }) } static tick(callback) { if (this.currentTick) { cancelAnimationFrame(this.currentTick); console.log("stop"); } const loop = () => { if (!callback() || this.stop) { console.log(this.stop ? "stop" : "end"); this.data = null; this.currentTick = null; Typing.data.forEach(e => { if (!e.isInterrupt) { this.sentences[e.index].style = ""; e.isMiss && this.sentences[e.index].classList.add("miss"); } }) return; } this.currentTick = requestAnimationFrame(loop); }; this.currentTick = requestAnimationFrame(loop); } static #handleKeydown = e => { switch (e.key) { case "a": document.getElementById("miss_only_btn").click(); break; case "Escape": this.stop = true; this.sentences && Typing.data.forEach(e => { !e.isMiss && !e.isInterrupt && (this.sentences[e.index].textContent = e.char); e.isMiss && this.sentences[e.index].classList.add("miss"); this.sentences[e.index].style = e.isInterrupt ? "opacity: 0.6; display: inline;" : ""; this.sentences[e.index].classList.remove("entered"); }) Result.selected && (Result.selected = null, document.getElementsByClassName("selected")[0]?.classList.remove("selected")); document.getElementsByClassName("hover")[0]?.classList.remove("hover"); Result.fixed = false; document.getElementsByClassName("result_data")[1].classList.remove("fixed"); setTimeout(() => Result.prev.innerHTML = Result.savePrev); break; } } static clear(){ this.stop = false; this.data = null; this.sentence = null; this.sentences = null; document.removeEventListener("keydown", this.#handleKeydown); parent.document.removeEventListener("keydown", this.#handleKeydown); } } class Typing { 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"]; static data = []; static score(data = this.data){ const ms = this.data.at(-1).time; const typingCount = this.typingCount(data); const missTypeCount = this.missTypeCount(data); const correctRate = Math.floor(Math.max(10000 * (typingCount - missTypeCount) / typingCount, 0)); return 60000 * (typingCount - missTypeCount) / ms * (correctRate / 10000) ** 2; } static level(score){ 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]; } static inputTime(data = this.data){ const ms = data.at(-1).time; return (ms < 60000 ? "" : Math.floor(ms / 60000) + "分") + (ms / 1000 % 60).toFixed(2).replace(".","秒"); } static typingCount(data = this.data){ return data.filter(e => !e.isMiss && !e.isInterrupt).length; } static missTypeCount(data = this.data){ return data.filter(e => e.isMiss && !e.isInterrupt).length; } static wpm(data = this.data){ const ms = this.data.at(-1).time; const typingCount = this.typingCount(data); return Math.floor(typingCount * (6000000 / ms)) / 100; } static correctRate(data = this.data){ const typingCount = this.typingCount(data); const missTypeCount = this.missTypeCount(data); return Math.floor(Math.max(10000 * (typingCount - missTypeCount) / typingCount, 0)); } static latency(data = this.data){ return data.find(e => !e.isMiss && !e.isInterrupt)?.time || NaN; } static rkpm(data = this.data){ const ms = this.data.at(-1).time; const typingCount = this.typingCount(data); const latency = this.latency(data); return (typingCount - 1) / (ms - latency) * 60000 || 0; } static result(start = 0, end = this.data.length, indexBreak){ const data = this.dataSlice(start, end, indexBreak); const latency = this.latency(data); const ms = data.at(-1).time; const inputTime = (ms < 60000 ? "" : Math.floor(ms / 60000) + "分") + (ms / 1000 % 60).toFixed(2).replace(".","秒"); const typingCount = this.typingCount(data); const wpm = Math.floor(typingCount * (6000000 / ms)) / 100; const missTypeCount = this.missTypeCount(data); const correctRate = Math.floor(Math.max(10000 * (typingCount - missTypeCount) / typingCount, 0)); const score = 60000 * (typingCount - missTypeCount) / ms * (correctRate / 10000) ** 2; const level = this.level(score); const rkpm = (typingCount - 1) / (ms - latency) * 60000 || 0; return { score: score, level: level, inputTime: inputTime, typingCount: typingCount, missTypeCount: missTypeCount, wpm: wpm, correctRate: correctRate, latency: latency, rkpm: rkpm } } static dataSlice(start, end, indexBreak){ 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); 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 })); } static latestIndex(){ return this.data.at(-1).index; } static clear(){ this.data = []; } }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址