Added ability to watch replay

This commit is contained in:
lukew3 2021-05-08 15:37:29 -04:00
parent 7acd5d9f2b
commit d4e954069e
9 changed files with 14455 additions and 42 deletions

File diff suppressed because it is too large Load diff

View file

@ -161,6 +161,7 @@ const refactoredSrc = [
"./src/js/test/test-timer.js",
"./src/js/test/test-config.js",
"./src/js/test/layout-emulator.js",
"./src/js/replay.js",
];
//legacy files

10519
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@ import * as TimerProgress from "./timer-progress";
import * as TestTimer from "./test-timer";
import * as Focus from "./focus";
import * as ShiftTracker from "./shift-tracker";
import * as Replay from "./replay.js";
$("#wordsInput").keypress((event) => {
event.preventDefault();
@ -130,6 +131,7 @@ function handleBackspace(event) {
}
}
TestLogic.words.decreaseCurrentIndex();
Replay.addReplayEvent("backWord");
TestUI.setCurrentWordElementIndex(TestUI.currentWordElementIndex - 1);
TestUI.updateActiveElement(true);
Funbox.toggleScript(TestLogic.words.getCurrent());
@ -138,6 +140,7 @@ function handleBackspace(event) {
} else {
if (Config.confidenceMode === "max") return;
if (event["ctrlKey"] || event["altKey"]) {
Replay.addReplayEvent("clearWord");
let limiter = " ";
if (
TestLogic.input.current.lastIndexOf("-") >
@ -165,6 +168,7 @@ function handleBackspace(event) {
TestLogic.input.setCurrent(
TestLogic.input.current.substring(0, TestLogic.input.current.length - 1)
);
Replay.addReplayEvent("deleteLetter");
}
TestUI.updateWordElement(!Config.blindMode);
}
@ -230,6 +234,7 @@ function handleSpace(event, isEnter) {
dontInsertSpace = true;
if (currentWord == TestLogic.input.current || Config.mode == "zen") {
//correct word or in zen mode
Replay.addReplayEvent("submitCorrectWord");
PaceCaret.handleSpace(true, currentWord);
TestStats.incrementAccuracy(true);
TestLogic.input.pushHistory();
@ -247,6 +252,7 @@ function handleSpace(event, isEnter) {
}
} else {
//incorrect word
Replay.addReplayEvent("submitErrorWord");
PaceCaret.handleSpace(false, currentWord);
if (Funbox.active !== "nospace") {
if (!Config.playSoundOnError || Config.blindMode) {
@ -566,6 +572,7 @@ function handleAlpha(event) {
}
if (!thisCharCorrect) {
Replay.addReplayEvent("incorrectLetter", event.key);
TestStats.incrementAccuracy(false);
TestStats.incrementKeypressErrors();
// currentError.count++;
@ -573,6 +580,7 @@ function handleAlpha(event) {
thisCharCorrect = false;
TestStats.pushMissedWord(TestLogic.words.getCurrent());
} else {
Replay.addReplayEvent("correctLetter", event.key);
TestStats.incrementAccuracy(true);
thisCharCorrect = true;
if (Config.mode == "zen") {

267
src/js/replay.js Normal file
View file

@ -0,0 +1,267 @@
/*
TODO:
Export replay as video
Export replay as typing test file?
.ttr file extension (stands for typing test record)
Should just be json, but fields should be specified by some format
metadata field with rules, website source, mode, name of typist
data field should be a list of objects, like monkeytype replay uses
signature or verfication field should be able to check file validity with server
And add ability to upload file to watch replay
*/
let wordsList = [];
let replayData = [];
let replayStartTime = 0;
let replayRecording = true;
let wordPos = 0;
let curPos = 0;
let targetWordPos = 0;
let targetCurPos = 0;
let timeoutList = [];
const toggleButton = document.getElementById("playpauseReplayButton")
.children[0];
function replayGetWordsList(wordsListFromScript) {
wordsList = wordsListFromScript;
}
function initializeReplayPrompt() {
const replayWordsElement = document.getElementById("replayWords");
replayWordsElement.innerHTML = "";
let wordCount = 0;
replayData.forEach((item, i) => {
//trim wordsList for timed tests
if (item.action === "backWord") {
wordCount--;
} else if (
item.action === "submitCorrectWord" ||
item.action === "submitErrorWord"
) {
wordCount++;
}
});
wordsList.forEach((item, i) => {
if (i > wordCount) return;
let x = document.createElement("div");
x.className = "word";
for (i = 0; i < item.length; i++) {
let letter = document.createElement("LETTER");
letter.innerHTML = item[i];
x.appendChild(letter);
}
replayWordsElement.appendChild(x);
});
}
function startReplayRecording() {
if (!$("#resultReplay").stop(true, true).hasClass("hidden")) {
//hide replay display if user left it open
toggleReplayDisplay();
}
replayData = [];
replayStartTime = performance.now();
replayRecording = true;
targetCurPos = 0;
targetWordPos = 0;
}
function stopReplayRecording() {
replayRecording = false;
}
function addReplayEvent(action, letter = undefined) {
if (replayRecording === false) {
return;
}
let timeDelta = performance.now() - replayStartTime;
if (action === "incorrectLetter" || action === "correctLetter") {
replayData.push({ action: action, letter: letter, time: timeDelta });
} else {
replayData.push({ action: action, time: timeDelta });
}
}
function pauseReplay() {
timeoutList.forEach((item, i) => {
clearTimeout(item);
});
timeoutList = [];
targetCurPos = curPos;
targetWordPos = wordPos;
toggleButton.className = "fas fa-play";
toggleButton.parentNode.setAttribute("aria-label", "Resume replay");
}
function loadOldReplay() {
let startingIndex = 0;
curPos = 0;
wordPos = 0;
replayData.forEach((item, i) => {
if (
wordPos < targetWordPos ||
(wordPos === targetWordPos && curPos < targetCurPos)
) {
//quickly display everything up to the target
handleDisplayLogic(item);
startingIndex = i + 1;
}
});
return startingIndex;
}
function playReplay() {
curPos = 0;
wordPos = 0;
toggleButton.className = "fas fa-pause";
toggleButton.parentNode.setAttribute("aria-label", "Pause replay");
initializeReplayPrompt();
let startingIndex = loadOldReplay();
let lastTime = replayData[startingIndex].time;
replayData.forEach((item, i) => {
if (i < startingIndex) return;
timeoutList.push(
setTimeout(() => {
handleDisplayLogic(item);
}, item.time - lastTime)
);
});
timeoutList.push(
setTimeout(() => {
//after the replay has finished, this will run
targetCurPos = 0;
targetWordPos = 0;
toggleButton.className = "fas fa-play";
toggleButton.parentNode.setAttribute("aria-label", "Start replay");
}, replayData[replayData.length - 1].time - lastTime)
);
}
function handleDisplayLogic(item) {
let activeWord = document.getElementById("replayWords").children[wordPos];
if (item.action === "correctLetter") {
activeWord.children[curPos].classList.add("correct");
curPos++;
} else if (item.action === "incorrectLetter") {
let myElement;
if (curPos >= activeWord.children.length) {
//if letter is an extra
myElement = document.createElement("letter");
myElement.classList.add("extra");
myElement.innerHTML = item.letter;
activeWord.appendChild(myElement);
}
myElement = activeWord.children[curPos];
myElement.classList.add("incorrect");
curPos++;
} else if (item.action === "deleteLetter") {
let myElement = activeWord.children[curPos - 1];
if (myElement.classList.contains("extra")) {
myElement.remove();
} else {
myElement.className = "";
}
curPos--;
} else if (item.action === "submitCorrectWord") {
wordPos++;
curPos = 0;
} else if (item.action === "submitErrorWord") {
activeWord.classList.add("error");
wordPos++;
curPos = 0;
} else if (item.action === "clearWord") {
let promptWord = document.createElement("div");
let wordArr = wordsList[wordPos].split("");
wordArr.forEach((letter, i) => {
promptWord.innerHTML += `<letter>${letter}</letter>`;
});
activeWord.innerHTML = promptWord.innerHTML;
curPos = 0;
} else if (item.action === "backWord") {
wordPos--;
activeWord = document.getElementById("replayWords").children[wordPos];
curPos = activeWord.children.length;
while (activeWord.children[curPos - 1].className === "") curPos--;
activeWord.classList.remove("error");
}
}
function toggleReplayDisplay() {
if ($("#resultReplay").stop(true, true).hasClass("hidden")) {
initializeReplayPrompt();
loadOldReplay();
//show
if (!$("#watchReplayButton").hasClass("loaded")) {
$("#words").html(
`<div class="preloader"><i class="fas fa-fw fa-spin fa-circle-notch"></i></div>`
);
$("#resultReplay")
.removeClass("hidden")
.css("display", "none")
.slideDown(250);
} else {
$("#resultReplay")
.removeClass("hidden")
.css("display", "none")
.slideDown(250);
}
} else {
//hide
if (toggleButton.parentNode.getAttribute("aria-label") != "Start replay") {
pauseReplay();
}
$("#resultReplay").slideUp(250, () => {
$("#resultReplay").addClass("hidden");
});
}
}
$(".pageTest #playpauseReplayButton").click(async (event) => {
if (toggleButton.className === "fas fa-play") {
playReplay();
} else if (toggleButton.className === "fas fa-pause") {
pauseReplay();
}
});
$("#replayWords").click((event) => {
//allows user to click on the place they want to start their replay at
pauseReplay();
const replayWords = document.querySelector("#replayWords");
let range;
let textNode;
if (document.caretPositionFromPoint) {
// standard
range = document.caretPositionFromPoint(event.pageX, event.pageY);
textNode = range.offsetNode;
} else if (document.caretRangeFromPoint) {
// WebKit
range = document.caretRangeFromPoint(event.pageX, event.pageY);
textNode = range.startContainer;
}
const words = [...replayWords.children];
targetWordPos = words.indexOf(textNode.parentNode.parentNode);
const letters = [...words[targetWordPos].children];
targetCurPos = letters.indexOf(textNode.parentNode);
initializeReplayPrompt();
loadOldReplay();
});
$(document).on("keypress", "#watchReplayButton", (event) => {
if (event.keyCode == 13) {
toggleReplayDisplay();
}
});
$(document.body).on("click", "#watchReplayButton", () => {
toggleReplayDisplay();
});
export {
startReplayRecording,
stopReplayRecording,
addReplayEvent,
replayGetWordsList,
};

View file

@ -27,6 +27,7 @@ import * as DB from "./db";
import * as ThemeColors from "./theme-colors";
import * as CloudFunctions from "./cloud-functions";
import * as TestLeaderboards from "./test-leaderboards";
import * as Replay from "./replay.js";
export let notSignedInLastResult = null;
@ -318,6 +319,8 @@ export function startTest() {
console.log("Analytics unavailable");
}
setActive(true);
Replay.startReplayRecording();
Replay.replayGetWordsList(words.list);
TestStats.resetKeypressTimings();
TimerProgress.restart();
TimerProgress.show();
@ -343,6 +346,7 @@ export function startTest() {
export async function init() {
setActive(false);
Replay.stopReplayRecording();
words.reset();
TestUI.setCurrentWordElementIndex(0);
// accuracy = {
@ -678,6 +682,7 @@ export function restart(withSameWordset = false, nosave = false, event) {
Focus.set(false);
Caret.hide();
setActive(false);
Replay.stopReplayRecording();
LiveWpm.hide();
LiveAcc.hide();
TimerProgress.hide();
@ -727,6 +732,7 @@ export function restart(withSameWordset = false, nosave = false, event) {
} else {
setRepeated(true);
setActive(false);
Replay.stopReplayRecording();
words.resetCurrentIndex();
input.reset();
PaceCaret.init();
@ -952,6 +958,7 @@ export function finish(difficultyFailed = false) {
if (Config.mode == "zen" && input.current.length != 0) {
input.pushHistory();
corrected.pushHistory();
Replay.replayGetWordsList(input.history);
}
TestStats.recordKeypressSpacing();
@ -960,6 +967,7 @@ export function finish(difficultyFailed = false) {
TestUI.setResultVisible(true);
TestStats.setEnd(performance.now());
setActive(false);
Replay.stopReplayRecording();
Focus.set(false);
Caret.hide();
LiveWpm.hide();

View file

@ -106,7 +106,7 @@
#result {
.buttons {
grid-template-rows: 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr;
#nextTestButton {
grid-column: 1/5;
width: 100%;
@ -219,7 +219,7 @@
}
#result {
.buttons {
grid-template-rows: 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr 1fr;
#nextTestButton {
grid-column: 1/3;
width: 100%;

View file

@ -1864,7 +1864,8 @@ key {
grid-column: 1/3;
}
#resultWordsHistory {
#resultWordsHistory,
#resultReplay {
// grid-area: wordsHistory;
color: var(--sub-color);
grid-column: 1/3;
@ -1876,8 +1877,18 @@ key {
user-select: none;
.word {
position: relative;
margin: 0.18rem 0.6rem 0.15rem 0;
}
}
.correct {
color: var(--text-color);
}
.incorrect {
color: var(--error-color);
}
.incorrect.extra {
color: var(--error-extra-color);
}
}
.chart {
@ -2276,7 +2287,8 @@ key {
#copyResultToClipboardButton,
#restartTestButtonWithSameWordset,
#nextTestButton,
#practiseMissedWordsButton {
#practiseMissedWordsButton,
#watchReplayButton {
position: relative;
border-radius: var(--roundness);
padding: 1rem 2rem;
@ -2297,6 +2309,10 @@ key {
}
}
#replayWords {
cursor: pointer;
}
#restartTestButton {
margin: 0 auto;
margin-top: 1rem;

View file

@ -1424,6 +1424,23 @@
</div>
<div class="words"></div>
</div>
<div id="resultReplay" class="hidden">
<div class="title">
watch replay
<span
id="playpauseReplayButton"
class="icon-button"
aria-label="Start replay"
data-balloon-pos="up"
style="display: inline-block"
>
<i class="fas fa-play"></i>
</span>
</div>
<div id="wordsWrapper">
<div id="replayWords" class="words"></div>
</div>
</div>
<div class="loginTip">
<a href="/login" tabindex="9">Sign in</a>
to save your results
@ -1475,6 +1492,15 @@
>
<i class="far fa-fw fa-image"></i>
</div>
<div
id="watchReplayButton"
aria-label="Watch Replay"
data-balloon-pos="down"
tabindex="0"
onclick="this.blur();"
>
<i class="fas fa-fw fa-backward"></i>
</div>
</div>
</div>
</div>