let wordsList = []; let currentWordIndex = 0; let currentWordElementIndex = 0; let inputHistory = []; let correctedHistory = []; let currentCorrected = ""; let currentInput = ""; let time = 0; let timer = null; let testActive = false; let testStart, testEnd; let wpmHistory = []; let rawHistory = []; let restartCount = 0; let incompleteTestSeconds = 0; let currentTestLine = 0; let pageTransition = false; let lineTransition = false; let keypressPerSecond = []; let currentKeypress = { count: 0, words: [], }; let errorsPerSecond = []; let currentError = { count: 0, words: [], }; let resultVisible = false; let activeWordTopBeforeJump = 0; let activeWordTop = 0; let activeWordJumped = false; let sameWordset = false; let quotes = []; let focusState = false; let activeFunBox = "none"; let manualRestart = false; let bailout = false; let notSignedInLastResult = null; let caretAnimating = true; let themeColors = { bg: "#323437", main: "#e2b714", caret: "#e2b714", sub: "#646669", text: "#d1d0c5", error: "#ca4754", errorExtra: "#7e2a33", colorfulError: "#ca4754", colorfulErrorExtra: "#7e2a33", }; let accuracyStats = { correct: 0, incorrect: 0, }; let keypressStats = { spacing: { current: -1, array: [], }, duration: { current: -1, array: [], }, }; let errorSound = new Audio("../sound/error.wav"); let clickSounds = null; function initClickSounds() { clickSounds = { "1": [ { sounds: [ new Audio("../sound/click1/click1_1.wav"), new Audio("../sound/click1/click1_1.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click1/click1_2.wav"), new Audio("../sound/click1/click1_2.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click1/click1_3.wav"), new Audio("../sound/click1/click1_3.wav"), ], counter: 0, }, ], "2": [ { sounds: [ new Audio("../sound/click2/click2_1.wav"), new Audio("../sound/click2/click2_1.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click2/click2_2.wav"), new Audio("../sound/click2/click2_2.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click2/click2_3.wav"), new Audio("../sound/click2/click2_3.wav"), ], counter: 0, }, ], "3": [ { sounds: [ new Audio("../sound/click3/click3_1.wav"), new Audio("../sound/click3/click3_1.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click3/click3_2.wav"), new Audio("../sound/click3/click3_2.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click3/click3_3.wav"), new Audio("../sound/click3/click3_3.wav"), ], counter: 0, }, ], "4": [ { sounds: [ new Audio("../sound/click4/click4_1.wav"), new Audio("../sound/click4/click4_1.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click4/click4_2.wav"), new Audio("../sound/click4/click4_2.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click4/click4_3.wav"), new Audio("../sound/click4/click4_3.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click4/click4_4.wav"), new Audio("../sound/click4/click4_4.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click4/click4_5.wav"), new Audio("../sound/click4/click4_5.wav"), ], counter: 0, }, { sounds: [ new Audio("../sound/click4/click4_6.wav"), new Audio("../sound/click4/click4_6.wav"), ], counter: 0, }, ], }; } let customText = "The quick brown fox jumps over the lazy dog".split(" "); let customTextIsRandom = false; let customTextWordCount = 1; let randomQuote = null; const testCompleted = firebase.functions().httpsCallable("testCompleted"); const addTag = firebase.functions().httpsCallable("addTag"); const editTag = firebase.functions().httpsCallable("editTag"); const removeTag = firebase.functions().httpsCallable("removeTag"); const updateResultTags = firebase.functions().httpsCallable("updateResultTags"); const saveConfig = firebase.functions().httpsCallable("saveConfig"); const generatePairingCode = firebase .functions() .httpsCallable("generatePairingCode"); function refreshThemeColorObject() { let st = getComputedStyle(document.body); themeColors.bg = st.getPropertyValue("--bg-color").replace(" ", ""); themeColors.main = st.getPropertyValue("--main-color").replace(" ", ""); themeColors.caret = st.getPropertyValue("--caret-color").replace(" ", ""); themeColors.sub = st.getPropertyValue("--sub-color").replace(" ", ""); themeColors.text = st.getPropertyValue("--text-color").replace(" ", ""); themeColors.error = st.getPropertyValue("--error-color").replace(" ", ""); themeColors.errorExtra = st .getPropertyValue("--error-extra-color") .replace(" ", ""); themeColors.colorfulError = st .getPropertyValue("--colorful-error-color") .replace(" ", ""); themeColors.colorfulErrorExtra = st .getPropertyValue("--colorful-error-extra-color") .replace(" ", ""); } function copyResultToClipboard() { if (navigator.userAgent.toLowerCase().indexOf("firefox") > -1) { showNotification("Sorry, this feature is not supported in Firefox", 4000); } else { $(".pageTest .ssWatermark").removeClass("hidden"); $(".pageTest .buttons").addClass("hidden"); let src = $("#middle"); var sourceX = src.position().left; /*X position from div#target*/ var sourceY = src.position().top; /*Y position from div#target*/ var sourceWidth = src.width(); /*clientWidth/offsetWidth from div#target*/ var sourceHeight = src.height(); /*clientHeight/offsetHeight from div#target*/ $(".notification").addClass("hidden"); try { html2canvas(document.body, { backgroundColor: themeColors.bg, height: sourceHeight + 50, width: sourceWidth + 50, x: sourceX - 25, y: sourceY - 25, }).then(function (canvas) { // document.body.appendChild(canvas); canvas.toBlob(function (blob) { navigator.clipboard .write([ new ClipboardItem( Object.defineProperty({}, blob.type, { value: blob, enumerable: true, }) ), ]) .then((f) => { $(".notification").removeClass("hidden"); showNotification("Copied to clipboard", 1000); $(".pageTest .ssWatermark").addClass("hidden"); $(".pageTest .buttons").removeClass("hidden"); }) .catch((f) => { $(".notification").removeClass("hidden"); showNotification("Error saving image to clipboard", 2000); $(".pageTest .ssWatermark").addClass("hidden"); $(".pageTest .buttons").removeClass("hidden"); }); }); }); } catch (e) { $(".notification").removeClass("hidden"); showNotification("Error creating image", 2000); $(".pageTest .ssWatermark").addClass("hidden"); $(".pageTest .buttons").removeClass("hidden"); } } } function activateFunbox(funbox, mode) { if (testActive || resultVisible) { showNotification( "You can only change the funbox before starting a test.", 4000 ); return false; } $("#funBoxTheme").attr("href", ``); if (funbox === "none") { activeFunBox = "none"; } if (mode === "style") { if (funbox != undefined) { $("#funBoxTheme").attr("href", `funbox/${funbox}.css`); activeFunBox = funbox; } if (funbox === "simon_says") { changeKeymapMode("next"); settingsGroups.keymapMode.updateButton(); restartTest(); } } else if (mode === "script") { if (funbox === "tts") { $("#funBoxTheme").attr("href", `funbox/simon_says.css`); config.keymapMode = "off"; settingsGroups.keymapMode.updateButton(); restartTest(); } else if (funbox === "layoutfluid") { config.keymapMode = "on"; changeKeymapMode("next"); settingsGroups.keymapMode.updateButton(); changeLayout("qwerty"); settingsGroups.layout.updateButton(); changeKeymapLayout("qwerty"); settingsGroups.keymapLayout.updateButton(); restartTest(); } activeFunBox = funbox; } updateTestModesNotice(); return true; } function toggleScriptFunbox(...params) { if (activeFunBox === "tts") { var msg = new SpeechSynthesisUtterance(); // var voices = window.speechSynthesis.getVoices(); // msg.voice = voices[0]; // msg.volume = 1; // From 0 to 1 // msg.rate = 1; // From 0.1 to 10 msg.text = params[0]; msg.lang = "en-US"; window.speechSynthesis.cancel(); window.speechSynthesis.speak(msg); } } function getuid() { console.error("Only share this uid with Miodec and nobody else!"); console.log(firebase.auth().currentUser.uid); console.error("Only share this uid with Miodec and nobody else!"); } function setFocus(foc) { if (foc && !focusState) { focusState = true; stopCaretAnimation(); $("#top").addClass("focus"); $("#bottom").addClass("focus"); $("body").css("cursor", "none"); $("#middle").addClass("focus"); } else if (!foc && focusState) { focusState = false; if (testActive) { stopCaretAnimation(); } else { startCaretAnimation(); } $("#top").removeClass("focus"); $("#bottom").removeClass("focus"); $("body").css("cursor", "default"); $("#middle").removeClass("focus"); } } function initWords() { testActive = false; wordsList = []; currentWordIndex = 0; currentWordElementIndex = 0; accuracyStats = { correct: 0, incorrect: 0, }; inputHistory = []; correctedHistory = []; currentCorrected = ""; currentInput = ""; let language = words[config.language]; if (language === undefined && config.language === "english_10k") { showBackgroundLoader(); $.ajax({ url: "js/english_10k.json", async: false, success: function (data) { hideBackgroundLoader(); words["english_10k"] = { leftToRight: true, words: data, }; language = words[config.language]; }, }); } if (config.mode === "quote" && quotes.length == 0) { showBackgroundLoader(); $.ajax({ url: "js/english_quotes.json", async: false, success: function (data) { hideBackgroundLoader(); quotes = data; }, }); } if (!language) { config.language = "english"; language = words[config.language]; } if ( config.mode == "time" || config.mode == "words" || config.mode == "custom" ) { // let wordsBound = config.mode == "time" ? 60 : config.words; let wordsBound = 60; if (config.showAllLines) { if (config.mode === "custom") { if (customTextIsRandom) { wordsBound = customTextWordCount; } else { wordsBound = customText.length; } } else if (config.mode != "time") { wordsBound = config.words; } } else { if (config.mode === "words" && config.words < wordsBound) { wordsBound = config.words; } if ( config.mode == "custom" && customTextIsRandom && customTextWordCount < wordsBound ) { wordsBound = customTextWordCount; } if ( config.mode == "custom" && !customTextIsRandom && customText.length < wordsBound ) { wordsBound = customText.length; } } if (activeFunBox === "plus_one") { wordsBound = 2; } let wordset = language.words; if (config.mode == "custom") { wordset = customText; } for (let i = 0; i < wordsBound; i++) { randomWord = wordset[Math.floor(Math.random() * wordset.length)]; previousWord = wordsList[i - 1]; previousWord2 = wordsList[i - 2]; if (config.mode == "custom" && wordset.length < 3 && customTextIsRandom) { randomWord = wordset[Math.floor(Math.random() * wordset.length)]; } else if (config.mode == "custom" && !customTextIsRandom) { randomWord = customText[i]; } else { while ( randomWord == previousWord || randomWord == previousWord2 || (!config.punctuation && randomWord == "I") || randomWord.indexOf(" ") > -1 ) { randomWord = wordset[Math.floor(Math.random() * wordset.length)]; } } if (activeFunBox === "rAnDoMcAsE") { let randomcaseword = ""; for (let i = 0; i < randomWord.length; i++) { if (i % 2 != 0) { randomcaseword += randomWord[i].toUpperCase(); } else { randomcaseword += randomWord[i]; } } randomWord = randomcaseword; } else if (activeFunBox === "gibberish") { randomWord = getGibberish(); } else if (activeFunBox === "58008") { setToggleSettings(false); randomWord = getNumbers(7); } else if (activeFunBox === "specials") { setToggleSettings(false); randomWord = getSpecials(); } else if (activeFunBox === "ascii") { setToggleSettings(false); randomWord = getASCII(); } if (config.punctuation && config.mode != "custom") { randomWord = punctuateWord(previousWord, randomWord, i, wordsBound); } if (config.numbers && config.mode != "custom") { if (Math.random() < 0.1) { randomWord = getNumbers(4); } } wordsList.push(randomWord); } } else if (config.mode == "quote") { randomQuote = quotes[Math.floor(Math.random() * quotes.length)]; let w = randomQuote.text.trim().split(" "); for (let i = 0; i < w.length; i++) { wordsList.push(w[i]); } } //handle right-to-left languages if (language.leftToRight) { arrangeCharactersLeftToRight(); } else { arrangeCharactersRightToLeft(); } showWords(); } function arrangeCharactersRightToLeft() { $("#words").addClass("rightToLeftTest"); } function arrangeCharactersLeftToRight() { $("#words").removeClass("rightToLeftTest"); } function setToggleSettings(state) { setPunctuation(state); setNumbers(state); } function emulateLayout(event) { function emulatedLayoutShouldShiftKey(event, newKeyPreview) { if (config.capsLockBackspace) return event.shiftKey; const isCapsLockHeld = event.originalEvent.getModifierState("CapsLock"); if (isCapsLockHeld) return isASCIILetter(newKeyPreview) !== event.shiftKey; return event.shiftKey; } function replaceEventKey(event, keyCode) { const newKey = String.fromCharCode(keyCode); event.keyCode = keyCode; event.charCode = keyCode; event.which = keyCode; event.key = newKey; event.code = "Key" + newKey.toUpperCase(); } if (event.key === " " || event.key === "Enter") return event; if (config.layout === "default") { //override the caps lock modifier for the default layout if needed if (config.capsLockBackspace && isASCIILetter(event.key)) { replaceEventKey( event, event.shiftKey ? event.key.toUpperCase().charCodeAt(0) : event.key.toLowerCase().charCodeAt(0) ); } return event; } const qwertyMasterLayout = { Backquote: "`~", Digit1: "1!", Digit2: "2@", Digit3: "3#", Digit4: "4$", Digit5: "5%", Digit6: "6^", Digit7: "7&", Digit8: "8*", Digit9: "9(", Digit0: "0)", Minus: "-_", Equal: "=+", KeyQ: "qQ", KeyW: "wW", KeyE: "eE", KeyR: "rR", KeyT: "tT", KeyY: "yY", KeyU: "uU", KeyI: "iI", KeyO: "oO", KeyP: "pP", BracketLeft: "[{", BracketRight: "]}", KeyA: "aA", KeyS: "sS", KeyD: "dD", KeyF: "fF", KeyG: "gG", KeyH: "hH", KeyJ: "jJ", KeyK: "kK", KeyL: "lL", Semicolon: ";:", Quote: "'\"", Backslash: "\\|", KeyZ: "zZ", KeyX: "xX", KeyC: "cC", KeyV: "vV", KeyB: "bB", KeyN: "nN", KeyM: "mM", Comma: ",<", Period: ".>", Slash: "/?", Space: " ", }; const layoutMap = layouts[config.layout]; const qwertyMap = layouts["qwerty"]; const qwertyKey = qwertyMasterLayout[event.code]; let mapIndex; for (let i = 0; i < qwertyMap.length; i++) { const key = qwertyMap[i]; const keyIndex = key.indexOf(qwertyKey); if (keyIndex != -1) { mapIndex = i; } } const newKeyPreview = layoutMap[mapIndex][0]; const shift = emulatedLayoutShouldShiftKey(event, newKeyPreview) ? 1 : 0; const newKey = layoutMap[mapIndex][shift]; replaceEventKey(event, newKey.charCodeAt(0)); return event; } function punctuateWord(previousWord, currentWord, index, maxindex) { let word = currentWord; if ( index == 0 || getLastChar(previousWord) == "." || getLastChar(previousWord) == "?" || getLastChar(previousWord) == "!" ) { //always capitalise the first word or if there was a dot word = capitalizeFirstLetter(word); } else if ( //10% chance to end a sentence (Math.random() < 0.1 && getLastChar(previousWord) != "." && index != maxindex - 2) || index == maxindex - 1 ) { let rand = Math.random(); if (rand <= 0.8) { word += "."; } else if (rand > 0.8 && rand < 0.9) { word += "?"; } else { word += "!"; } } else if ( Math.random() < 0.01 && getLastChar(previousWord) != "," && getLastChar(previousWord) != "." ) { //1% chance to add quotes word = `"${word}"`; } else if (Math.random() < 0.01) { //1% chance to add a colon word = word + ":"; } else if ( Math.random() < 0.01 && getLastChar(previousWord) != "," && getLastChar(previousWord) != "." && previousWord != "-" ) { //1% chance to add a dash word = "-"; } else if (Math.random() < 0.2 && getLastChar(previousWord) != ",") { //2% chance to add a comma word += ","; } return word; } function addWord() { let bound = 60; if (activeFunBox === "plus_one") bound = 1; if ( !config.showAllLines && (wordsList.length - inputHistory.length > bound || (config.mode === "words" && wordsList.length >= config.words) || (config.mode === "custom" && customTextIsRandom && wordsList.length >= customTextWordCount) || (config.mode === "custom" && !customTextIsRandom && wordsList.length >= customText.length)) ) return; const language = config.mode !== "custom" ? words[config.language] : { //borrow the direction of the current language leftToRight: words[config.language].leftToRight, words: customText, }; const wordset = language.words; let randomWord = wordset[Math.floor(Math.random() * wordset.length)]; previousWord = wordsList[wordsList.length - 1]; previousWordStripped = previousWord.replace(/[.?!":\-,]/g, "").toLowerCase(); previousWord2Stripped = wordsList[wordsList.length - 2] .replace(/[.?!":\-,]/g, "") .toLowerCase(); if (config.mode === "custom" && customTextIsRandom && wordset.length < 3) { randomWord = wordset[Math.floor(Math.random() * wordset.length)]; } else if (config.mode == "custom" && !customTextIsRandom) { randomWord = customText[wordsList.length]; } else { while ( previousWordStripped == randomWord || previousWord2Stripped == randomWord || randomWord.indexOf(" ") > -1 || (!config.punctuation && randomWord == "I") ) { randomWord = wordset[Math.floor(Math.random() * wordset.length)]; } } if (activeFunBox === "rAnDoMcAsE") { let randomcaseword = ""; for (let i = 0; i < randomWord.length; i++) { if (i % 2 != 0) { randomcaseword += randomWord[i].toUpperCase(); } else { randomcaseword += randomWord[i]; } } randomWord = randomcaseword; } else if (activeFunBox === "gibberish") { randomWord = getGibberish(); } else if (activeFunBox === "58008") { randomWord = getNumbers(7); } else if (activeFunBox === "specials") { randomWord = getSpecials(); } else if (activeFunBox === "ascii") { randomWord = getASCII(); } if (config.punctuation && config.mode != "custom") { randomWord = punctuateWord(previousWord, randomWord, wordsList.length, 0); } if (config.numbers && config.mode != "custom") { if (Math.random() < 0.1) { randomWord = getNumbers(4); } } wordsList.push(randomWord); let w = "