mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2026-02-04 14:39:02 +08:00
refactor: use animejs instead of jquery (@miodec) (#7101)
Also changes how slow timer is handled - now the animation frame rate is reduced to 30fps instead of disabling them entirely.
This commit is contained in:
parent
1009791915
commit
2536087276
44 changed files with 1043 additions and 1135 deletions
|
|
@ -93,6 +93,7 @@
|
|||
"@sentry/browser": "9.14.0",
|
||||
"@sentry/vite-plugin": "3.3.1",
|
||||
"@ts-rest/core": "3.52.1",
|
||||
"animejs": "4.2.2",
|
||||
"balloon-css": "1.2.0",
|
||||
"canvas-confetti": "1.5.1",
|
||||
"chart.js": "3.7.1",
|
||||
|
|
@ -108,8 +109,6 @@
|
|||
"howler": "2.2.3",
|
||||
"idb": "8.0.3",
|
||||
"jquery": "3.7.1",
|
||||
"jquery-color": "2.2.0",
|
||||
"jquery.easing": "1.4.1",
|
||||
"konami": "1.7.0",
|
||||
"lz-ts": "1.1.2",
|
||||
"modern-screenshot": "4.6.5",
|
||||
|
|
|
|||
|
|
@ -1887,6 +1887,35 @@
|
|||
<button>open</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section fpsLimit">
|
||||
<div class="groupTitle">
|
||||
<i class="fas fa-video"></i>
|
||||
<span>animation fps limit</span>
|
||||
<button class="text" tabindex="-1">
|
||||
<i class="fas fa-fw fa-link"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text">
|
||||
Limit the maximum fps for animations. Setting this to "native" will run
|
||||
the animations as fast as possible (at your monitor's refresh rate).
|
||||
Setting this above your monitor's refresh rate will have no effect.
|
||||
</div>
|
||||
<!-- <div class="inputs">
|
||||
<div class="rangeGroup">
|
||||
<div class="value">---</div>
|
||||
<input type="range" min="30" max="1000" step="10" />
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="inputs">
|
||||
<button data-fpsLimit="native">native</button>
|
||||
<div class="separator">
|
||||
<div class="line"></div>
|
||||
or
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<input type="number" placeholder="custom limit" min="30" max="1000" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="section resetSettings" data-section-id="resetSettings">
|
||||
<div class="groupTitle">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@
|
|||
}
|
||||
|
||||
.active {
|
||||
animation: accountRowHighlight 4s linear 0s 1;
|
||||
animation: accountRowHighlight 5s linear 0s 1;
|
||||
}
|
||||
|
||||
.loadMoreButton {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ nav {
|
|||
}
|
||||
|
||||
.level {
|
||||
transition: 0.125s;
|
||||
width: max-content;
|
||||
font-size: 0.65em;
|
||||
line-height: 0.65em;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@
|
|||
width: 350px;
|
||||
z-index: 99999999;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
// gap: 1rem;
|
||||
// margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
// top: 1rem;
|
||||
transition: 0.125s opacity;
|
||||
|
||||
.clearAll.button {
|
||||
font-size: 0.75em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
&.focus .clearAll {
|
||||
visibility: hidden;
|
||||
|
|
@ -22,13 +25,14 @@
|
|||
}
|
||||
.history {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
// gap: 1rem;
|
||||
}
|
||||
|
||||
.notif {
|
||||
--notif-border-color: rgba(0, 130, 251, 0.985);
|
||||
--notif-background-color: rgba(0, 77, 148, 0.9);
|
||||
transition: 0.125s background;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
|
|
|
|||
|
|
@ -169,9 +169,11 @@
|
|||
}
|
||||
|
||||
&[data-config-name="fontFamily"],
|
||||
&[data-config-name="customBackgroundSize"] {
|
||||
&[data-config-name="customBackgroundSize"],
|
||||
&.fpsLimit {
|
||||
.separator {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
grid-column: span 2;
|
||||
// color: var(--sub-color);
|
||||
display: grid;
|
||||
|
|
@ -187,6 +189,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.fpsLimit {
|
||||
.inputs {
|
||||
button,
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-config-name="fontFamily"] {
|
||||
grid-template-areas:
|
||||
"title title"
|
||||
|
|
|
|||
|
|
@ -1264,6 +1264,7 @@
|
|||
border-radius: var(--roundness);
|
||||
z-index: 2;
|
||||
// width: max-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
.spacer {
|
||||
height: auto;
|
||||
|
|
@ -1271,11 +1272,6 @@
|
|||
border-radius: calc(var(--roundness) / 2);
|
||||
background: var(--bg-color);
|
||||
margin: 0.75em 0;
|
||||
transition: 250ms;
|
||||
&.scrolled {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wordCount,
|
||||
|
|
@ -1334,6 +1330,12 @@
|
|||
transition: opacity 0.25s, right 0.25s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mode {
|
||||
background: var(--sub-alt-color);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.shareButton {
|
||||
opacity: 1;
|
||||
|
|
|
|||
37
frontend/src/ts/anim.ts
Normal file
37
frontend/src/ts/anim.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { engine } from "animejs";
|
||||
import { LocalStorageWithSchema } from "./utils/local-storage-with-schema";
|
||||
import { z } from "zod";
|
||||
|
||||
export const fpsLimitSchema = z.number().int().min(30).max(1000);
|
||||
|
||||
const fpsLimit = new LocalStorageWithSchema({
|
||||
key: "fpsLimit",
|
||||
schema: fpsLimitSchema,
|
||||
fallback: 1000,
|
||||
});
|
||||
|
||||
export function setfpsLimit(fps: number): boolean {
|
||||
const result = fpsLimit.set(fps);
|
||||
applyEngineSettings();
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getfpsLimit(): number {
|
||||
return fpsLimit.get();
|
||||
}
|
||||
|
||||
export function applyEngineSettings(): void {
|
||||
engine.pauseOnDocumentHidden = false;
|
||||
engine.fps = fpsLimit.get();
|
||||
engine.defaults.frameRate = fpsLimit.get();
|
||||
}
|
||||
|
||||
export function setLowFpsMode(): void {
|
||||
engine.fps = 30;
|
||||
engine.defaults.frameRate = 30;
|
||||
}
|
||||
|
||||
export function clearLowFpsMode(): void {
|
||||
engine.fps = fpsLimit.get();
|
||||
engine.defaults.frameRate = fpsLimit.get();
|
||||
}
|
||||
|
|
@ -577,6 +577,8 @@ async function updateActiveCommand(): Promise<void> {
|
|||
command.hover?.();
|
||||
}
|
||||
|
||||
let shakeTimeout: null | NodeJS.Timeout;
|
||||
|
||||
function handleInputSubmit(): void {
|
||||
if (isAnimating) return;
|
||||
if (inputModeParams.command === null) {
|
||||
|
|
@ -587,13 +589,13 @@ function handleInputSubmit(): void {
|
|||
//validation ongoing, ignore the submit
|
||||
return;
|
||||
} else if (inputModeParams.validation?.status === "failed") {
|
||||
const cmdLine = $("#commandLine .modal");
|
||||
cmdLine
|
||||
.stop(true, true)
|
||||
.addClass("hasError")
|
||||
.animate({ undefined: 1 }, 500, () => {
|
||||
cmdLine.removeClass("hasError");
|
||||
});
|
||||
modal.getModal().classList.add("hasError");
|
||||
if (shakeTimeout !== null) {
|
||||
clearTimeout(shakeTimeout);
|
||||
}
|
||||
shakeTimeout = setTimeout(() => {
|
||||
modal.getModal().classList.remove("hasError");
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,11 +54,9 @@ function updateTitle(nextPage: { id: string; display?: string }): void {
|
|||
async function showSyncLoading({
|
||||
loadingOptions,
|
||||
totalDuration,
|
||||
easingMethod,
|
||||
}: {
|
||||
loadingOptions: LoadingOptions[];
|
||||
totalDuration: number;
|
||||
easingMethod: Misc.JQueryEasing;
|
||||
}): Promise<void> {
|
||||
PageLoading.page.element.removeClass("hidden").css("opacity", 0);
|
||||
await PageLoading.page.beforeShow({});
|
||||
|
|
@ -67,14 +65,10 @@ async function showSyncLoading({
|
|||
const fillOffset = 100 / fillDivider;
|
||||
|
||||
//void here to run the loading promise as soon as possible
|
||||
void Misc.promiseAnimation(
|
||||
PageLoading.page.element,
|
||||
{
|
||||
opacity: "1",
|
||||
},
|
||||
totalDuration / 2,
|
||||
easingMethod
|
||||
);
|
||||
void Misc.promiseAnimate(PageLoading.page.element[0] as HTMLElement, {
|
||||
opacity: "1",
|
||||
duration: totalDuration / 2,
|
||||
});
|
||||
|
||||
for (let i = 0; i < loadingOptions.length; i++) {
|
||||
const currentOffset = fillOffset * i;
|
||||
|
|
@ -102,14 +96,10 @@ async function showSyncLoading({
|
|||
}
|
||||
}
|
||||
|
||||
await Misc.promiseAnimation(
|
||||
PageLoading.page.element,
|
||||
{
|
||||
opacity: "0",
|
||||
},
|
||||
totalDuration / 2,
|
||||
easingMethod
|
||||
);
|
||||
await Misc.promiseAnimate(PageLoading.page.element[0] as HTMLElement, {
|
||||
opacity: "0",
|
||||
duration: totalDuration / 2,
|
||||
});
|
||||
|
||||
await PageLoading.page.afterHide();
|
||||
PageLoading.page.element.addClass("hidden");
|
||||
|
|
@ -208,7 +198,6 @@ export async function change(
|
|||
const previousPage = pages[ActivePage.get()];
|
||||
const nextPage = pages[pageName];
|
||||
const totalDuration = Misc.applyReducedMotion(250);
|
||||
const easingMethod: Misc.JQueryEasing = "swing";
|
||||
|
||||
//start
|
||||
PageTransition.set(true);
|
||||
|
|
@ -217,14 +206,10 @@ export async function change(
|
|||
//previous page
|
||||
await previousPage?.beforeHide?.();
|
||||
previousPage.element.removeClass("hidden").css("opacity", 1);
|
||||
await Misc.promiseAnimation(
|
||||
previousPage.element,
|
||||
{
|
||||
opacity: "0",
|
||||
},
|
||||
totalDuration / 2,
|
||||
easingMethod
|
||||
);
|
||||
await Misc.promiseAnimate(previousPage.element[0] as HTMLElement, {
|
||||
opacity: "0",
|
||||
duration: totalDuration / 2,
|
||||
});
|
||||
previousPage.element.addClass("hidden");
|
||||
await previousPage?.afterHide();
|
||||
|
||||
|
|
@ -245,7 +230,6 @@ export async function change(
|
|||
await showSyncLoading({
|
||||
loadingOptions: syncLoadingOptions,
|
||||
totalDuration,
|
||||
easingMethod,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -297,14 +281,10 @@ export async function change(
|
|||
}
|
||||
|
||||
nextPage.element.removeClass("hidden").css("opacity", 0);
|
||||
await Misc.promiseAnimation(
|
||||
nextPage.element,
|
||||
{
|
||||
opacity: "1",
|
||||
},
|
||||
totalDuration / 2,
|
||||
easingMethod
|
||||
);
|
||||
await Misc.promiseAnimate(nextPage.element[0] as HTMLElement, {
|
||||
opacity: "1",
|
||||
duration: totalDuration / 2,
|
||||
});
|
||||
nextPage.element.addClass("active");
|
||||
await nextPage?.afterShow();
|
||||
|
||||
|
|
|
|||
|
|
@ -58,14 +58,14 @@ export function update(): void {
|
|||
`/profile/${name}`
|
||||
);
|
||||
void Misc.swapElements(
|
||||
$("nav .textButton.view-login"),
|
||||
$("nav .accountButtonAndMenu"),
|
||||
document.querySelector("nav .textButton.view-login") as HTMLElement,
|
||||
document.querySelector("nav .accountButtonAndMenu") as HTMLElement,
|
||||
250
|
||||
);
|
||||
} else {
|
||||
void Misc.swapElements(
|
||||
$("nav .accountButtonAndMenu"),
|
||||
$("nav .textButton.view-login"),
|
||||
document.querySelector("nav .accountButtonAndMenu") as HTMLElement,
|
||||
document.querySelector("nav .textButton.view-login") as HTMLElement,
|
||||
250,
|
||||
async () => {
|
||||
updateName("");
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { MonkeyMail } from "@monkeytype/schemas/users";
|
|||
import * as XPBar from "../elements/xp-bar";
|
||||
import * as AuthEvent from "../observables/auth-event";
|
||||
import * as ActivePage from "../states/active-page";
|
||||
import { animate } from "animejs";
|
||||
|
||||
let accountAlerts: MonkeyMail[] = [];
|
||||
let maxMail = 0;
|
||||
|
|
@ -341,18 +342,16 @@ function markReadAlert(id: string): void {
|
|||
.append(
|
||||
`<button class="deleteAlert textButton" aria-label="Delete" data-balloon-pos="left"><i class="fas fa-trash"></i></button>`
|
||||
);
|
||||
item.find(".rewards").animate(
|
||||
{
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
marginTop: 0,
|
||||
},
|
||||
250,
|
||||
"easeOutCubic",
|
||||
() => {
|
||||
|
||||
animate(item.find(".rewards")[0] as HTMLElement, {
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
marginTop: 0,
|
||||
duration: 250,
|
||||
onComplete: () => {
|
||||
item.find(".rewards").remove();
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateClaimDeleteAllButton(): void {
|
||||
|
|
@ -414,24 +413,12 @@ const modal = new AnimatedModal({
|
|||
customAnimations: {
|
||||
show: {
|
||||
modal: {
|
||||
from: {
|
||||
marginRight: "-10rem",
|
||||
},
|
||||
to: {
|
||||
marginRight: "0",
|
||||
},
|
||||
easing: "easeOutCirc",
|
||||
marginRight: ["-10rem", "0"],
|
||||
},
|
||||
},
|
||||
hide: {
|
||||
modal: {
|
||||
from: {
|
||||
marginRight: "0",
|
||||
},
|
||||
to: {
|
||||
marginRight: "-10rem",
|
||||
},
|
||||
easing: "easeInCirc",
|
||||
marginRight: ["0", "-10rem"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ export function validateWithIndicator<T>(
|
|||
inputElement.value = val ?? "";
|
||||
if (val === null) {
|
||||
indicator.hide();
|
||||
currentStatus = { status: "checking" };
|
||||
} else {
|
||||
inputElement.dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
|
@ -270,6 +271,8 @@ export function handleConfigInput<T extends ConfigKey>({
|
|||
});
|
||||
}
|
||||
|
||||
let shakeTimeout: null | NodeJS.Timeout;
|
||||
|
||||
const handleStore = (): void => {
|
||||
if (input.value === "" && (validation?.resetIfEmpty ?? true)) {
|
||||
//use last config value, clear validation
|
||||
|
|
@ -277,13 +280,13 @@ export function handleConfigInput<T extends ConfigKey>({
|
|||
input.dispatchEvent(new Event("input"));
|
||||
}
|
||||
if (status === "failed") {
|
||||
const parent = $(input.parentElement as HTMLElement);
|
||||
parent
|
||||
.stop(true, true)
|
||||
.addClass("hasError")
|
||||
.animate({ undefined: 1 }, 500, () => {
|
||||
parent.removeClass("hasError");
|
||||
});
|
||||
input.parentElement?.classList.add("hasError");
|
||||
if (shakeTimeout !== null) {
|
||||
clearTimeout(shakeTimeout);
|
||||
}
|
||||
shakeTimeout = setTimeout(() => {
|
||||
input.parentElement?.classList.remove("hasError");
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
const value = (inputValueConvert?.(input.value) ??
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import Config from "../config";
|
||||
import * as ThemeColors from "./theme-colors";
|
||||
import * as SlowTimer from "../states/slow-timer";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
import * as KeymapEvent from "../observables/keymap-event";
|
||||
import * as Misc from "../utils/misc";
|
||||
|
|
@ -16,6 +15,7 @@ import * as KeyConverter from "../utils/key-converter";
|
|||
import { getActiveFunboxNames } from "../test/funbox/list";
|
||||
import { areSortedArraysEqual } from "../utils/arrays";
|
||||
import { LayoutObject } from "@monkeytype/schemas/layouts";
|
||||
import { animate } from "animejs";
|
||||
|
||||
export const keyDataDelimiter = "~~";
|
||||
|
||||
|
|
@ -100,38 +100,33 @@ async function flashKey(key: string, correct?: boolean): Promise<void> {
|
|||
const themecolors = await ThemeColors.getAll();
|
||||
|
||||
try {
|
||||
let css = {
|
||||
let startingStyle = {
|
||||
color: themecolors.bg,
|
||||
backgroundColor: themecolors.sub,
|
||||
borderColor: themecolors.sub,
|
||||
};
|
||||
|
||||
if (correct || Config.blindMode) {
|
||||
css = {
|
||||
startingStyle = {
|
||||
color: themecolors.bg,
|
||||
backgroundColor: themecolors.main,
|
||||
borderColor: themecolors.main,
|
||||
};
|
||||
} else {
|
||||
css = {
|
||||
startingStyle = {
|
||||
color: themecolors.bg,
|
||||
backgroundColor: themecolors.error,
|
||||
borderColor: themecolors.error,
|
||||
};
|
||||
}
|
||||
|
||||
$(key)
|
||||
.stop(true, true)
|
||||
.css(css)
|
||||
.animate(
|
||||
{
|
||||
color: themecolors.sub,
|
||||
backgroundColor: themecolors.subAlt,
|
||||
borderColor: themecolors.sub,
|
||||
},
|
||||
SlowTimer.get() ? 0 : 500,
|
||||
"easeOutExpo"
|
||||
);
|
||||
animate(key, {
|
||||
color: [startingStyle.color, themecolors.sub],
|
||||
backgroundColor: [startingStyle.backgroundColor, themecolors.subAlt],
|
||||
borderColor: [startingStyle.borderColor, themecolors.sub],
|
||||
duration: 250,
|
||||
easing: "out(5)",
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { debounce } from "throttle-debounce";
|
||||
import * as Misc from "../utils/misc";
|
||||
import * as BannerEvent from "../observables/banner-event";
|
||||
// import * as Alerts from "./alerts";
|
||||
import * as NotificationEvent from "../observables/notification-event";
|
||||
import { convertRemToPixels } from "../utils/numbers";
|
||||
import { animate } from "animejs";
|
||||
|
||||
function updateMargin(): void {
|
||||
const height = $("#bannerCenter").height() as number;
|
||||
|
|
@ -13,6 +13,7 @@ function updateMargin(): void {
|
|||
|
||||
let visibleStickyNotifications = 0;
|
||||
let id = 0;
|
||||
|
||||
type NotificationType = "notification" | "banner" | "psa";
|
||||
class Notification {
|
||||
id: number;
|
||||
|
|
@ -99,56 +100,43 @@ class Notification {
|
|||
visibleStickyNotifications++;
|
||||
updateClearAllButton();
|
||||
}
|
||||
const oldHeight = $("#notificationCenter .history").height() as number;
|
||||
$("#notificationCenter .history").prepend(`
|
||||
<div class="notif ${cls}" id=${this.id} style="opacity: 0;">
|
||||
<div class="message"><div class="title"><div class="icon">${icon}</div>${title}</div>${this.message}</div>
|
||||
</div>
|
||||
`);
|
||||
const notif = document.querySelector<HTMLElement>(
|
||||
`#notificationCenter .notif[id='${this.id}']`
|
||||
);
|
||||
if (notif === null) return;
|
||||
|
||||
<div class="notif ${cls}" id=${this.id}>
|
||||
<div class="message"><div class="title"><div class="icon">${icon}</div>${title}</div>${this.message}</div>
|
||||
</div>
|
||||
const notifHeight = notif.offsetHeight;
|
||||
const duration = Misc.applyReducedMotion(250);
|
||||
|
||||
`);
|
||||
const newHeight = $("#notificationCenter .history").height() as number;
|
||||
$(`#notificationCenter .notif[id='${this.id}']`).remove();
|
||||
$("#notificationCenter .history")
|
||||
.css("margin-top", 0)
|
||||
.animate(
|
||||
{
|
||||
marginTop: newHeight - oldHeight,
|
||||
},
|
||||
Misc.applyReducedMotion(125),
|
||||
() => {
|
||||
$("#notificationCenter .history").css("margin-top", 0);
|
||||
$("#notificationCenter .history").prepend(`
|
||||
animate(notif, {
|
||||
opacity: [0, 1],
|
||||
duration: duration / 2,
|
||||
delay: duration / 2,
|
||||
});
|
||||
notif?.addEventListener("click", () => {
|
||||
this.hide();
|
||||
this.closeCallback();
|
||||
if (this.duration === 0) {
|
||||
visibleStickyNotifications--;
|
||||
}
|
||||
updateClearAllButton();
|
||||
});
|
||||
|
||||
<div class="notif ${cls}" id=${this.id}>
|
||||
<div class="message"><div class="title"><div class="icon">${icon}</div>${title}</div>${this.message}</div>
|
||||
</div>
|
||||
|
||||
`);
|
||||
$(`#notificationCenter .notif[id='${this.id}']`)
|
||||
.css("opacity", 0)
|
||||
.animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
Misc.applyReducedMotion(125),
|
||||
() => {
|
||||
$(`#notificationCenter .notif[id='${this.id}']`).css(
|
||||
"opacity",
|
||||
""
|
||||
);
|
||||
}
|
||||
);
|
||||
$(`#notificationCenter .notif[id='${this.id}']`).on("click", () => {
|
||||
this.hide();
|
||||
this.closeCallback();
|
||||
if (this.duration === 0) {
|
||||
visibleStickyNotifications--;
|
||||
}
|
||||
updateClearAllButton();
|
||||
});
|
||||
}
|
||||
);
|
||||
const historyElement = document.querySelector(
|
||||
"#notificationCenter .history"
|
||||
) as HTMLElement;
|
||||
animate(historyElement, {
|
||||
marginTop: {
|
||||
from: "-=" + notifHeight,
|
||||
to: 0,
|
||||
},
|
||||
duration: duration / 2,
|
||||
});
|
||||
$(`#notificationCenter .notif[id='${this.id}']`).on("hover", () => {
|
||||
$(`#notificationCenter .notif[id='${this.id}']`).toggleClass("hover");
|
||||
});
|
||||
|
|
@ -214,43 +202,37 @@ class Notification {
|
|||
}
|
||||
hide(): void {
|
||||
if (this.type === "notification") {
|
||||
$(`#notificationCenter .notif[id='${this.id}']`)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
Misc.applyReducedMotion(125),
|
||||
() => {
|
||||
$(`#notificationCenter .notif[id='${this.id}']`).animate(
|
||||
{
|
||||
height: 0,
|
||||
},
|
||||
Misc.applyReducedMotion(125),
|
||||
() => {
|
||||
$(`#notificationCenter .notif[id='${this.id}']`).remove();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
const elem = document.querySelector(
|
||||
`#notificationCenter .notif[id='${this.id}']`
|
||||
) as HTMLElement;
|
||||
|
||||
const duration = Misc.applyReducedMotion(250);
|
||||
|
||||
animate(elem, {
|
||||
opacity: {
|
||||
to: 0,
|
||||
duration: duration,
|
||||
},
|
||||
height: {
|
||||
to: 0,
|
||||
duration: duration / 2,
|
||||
delay: duration / 2,
|
||||
},
|
||||
marginBottom: {
|
||||
to: 0,
|
||||
duration: duration / 2,
|
||||
delay: duration / 2,
|
||||
},
|
||||
onComplete: () => {
|
||||
elem.remove();
|
||||
},
|
||||
});
|
||||
} else if (this.type === "banner" || this.type === "psa") {
|
||||
$(
|
||||
`#bannerCenter .banner[id='${this.id}'], #bannerCenter .psa[id='${this.id}']`
|
||||
)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
Misc.applyReducedMotion(125),
|
||||
() => {
|
||||
$(
|
||||
`#bannerCenter .banner[id='${this.id}'], #bannerCenter .psa[id='${this.id}']`
|
||||
).remove();
|
||||
updateMargin();
|
||||
BannerEvent.dispatch();
|
||||
}
|
||||
);
|
||||
).remove();
|
||||
updateMargin();
|
||||
BannerEvent.dispatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
56
frontend/src/ts/elements/settings/fps-limit-section.ts
Normal file
56
frontend/src/ts/elements/settings/fps-limit-section.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { getfpsLimit, fpsLimitSchema, setfpsLimit } from "../../anim";
|
||||
import { validateWithIndicator } from "../input-validation";
|
||||
import * as Notifications from "../notifications";
|
||||
|
||||
const section = document.querySelector(
|
||||
"#pageSettings .section.fpsLimit"
|
||||
) as HTMLElement;
|
||||
|
||||
const button = section.querySelector(
|
||||
"button[data-fpsLimit='native']"
|
||||
) as HTMLButtonElement;
|
||||
|
||||
const input = validateWithIndicator(
|
||||
section.querySelector('input[type="number"]') as HTMLInputElement,
|
||||
{
|
||||
schema: fpsLimitSchema,
|
||||
inputValueConvert: (val: string) => parseInt(val, 10),
|
||||
}
|
||||
);
|
||||
|
||||
export function update(): void {
|
||||
const fpsLimit = getfpsLimit();
|
||||
if (fpsLimit >= 1000) {
|
||||
input.setValue(null);
|
||||
button.classList.add("active");
|
||||
} else {
|
||||
input.value = fpsLimit.toString();
|
||||
button.classList.remove("active");
|
||||
}
|
||||
}
|
||||
|
||||
function save(value: number): void {
|
||||
if (setfpsLimit(value)) {
|
||||
Notifications.add("FPS limit updated", 0);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
function saveFromInput(): void {
|
||||
if (input.getValidationResult().status !== "success") return;
|
||||
const val = parseInt(input.value, 10);
|
||||
save(val);
|
||||
}
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
save(1000);
|
||||
update();
|
||||
});
|
||||
|
||||
input.addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
saveFromInput();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener("focusout", (e) => saveFromInput());
|
||||
|
|
@ -305,14 +305,22 @@ export function updateActiveTab(): void {
|
|||
|
||||
if (Config.customTheme) {
|
||||
void Misc.swapElements(
|
||||
$('.pageSettings [tabContent="preset"]'),
|
||||
$('.pageSettings [tabContent="custom"]'),
|
||||
document.querySelector(
|
||||
'.pageSettings [tabContent="preset"]'
|
||||
) as HTMLElement,
|
||||
document.querySelector(
|
||||
'.pageSettings [tabContent="custom"]'
|
||||
) as HTMLElement,
|
||||
250
|
||||
);
|
||||
} else {
|
||||
void Misc.swapElements(
|
||||
$('.pageSettings [tabContent="custom"]'),
|
||||
$('.pageSettings [tabContent="preset"]'),
|
||||
document.querySelector(
|
||||
'.pageSettings [tabContent="custom"]'
|
||||
) as HTMLElement,
|
||||
document.querySelector(
|
||||
'.pageSettings [tabContent="preset"]'
|
||||
) as HTMLElement,
|
||||
250
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import * as Misc from "../utils/misc";
|
||||
import * as Levels from "../utils/levels";
|
||||
import { getAll } from "./theme-colors";
|
||||
import * as SlowTimer from "../states/slow-timer";
|
||||
import { XpBreakdown } from "@monkeytype/schemas/results";
|
||||
import { isSafeNumber, mapRange } from "@monkeytype/util/numbers";
|
||||
import { isSafeNumber } from "@monkeytype/util/numbers";
|
||||
import { animate } from "animejs";
|
||||
|
||||
let breakdownVisible = false;
|
||||
let skip = false;
|
||||
|
|
@ -19,11 +19,15 @@ let lastUpdate: {
|
|||
breakdown: undefined,
|
||||
};
|
||||
|
||||
const xpBreakdownTotalEl = $("nav .xpBar .xpBreakdown .total");
|
||||
const xpBreakdownListEl = $("nav .xpBar .xpBreakdown .list");
|
||||
const levelEl = $("nav .level");
|
||||
const barEl = $("nav .xpBar .bar");
|
||||
const barWrapperEl = $("nav .xpBar");
|
||||
const xpBreakdownTotalEl = document.querySelector(
|
||||
"nav .xpBar .xpBreakdown .total"
|
||||
) as HTMLElement;
|
||||
const xpBreakdownListEl = document.querySelector(
|
||||
"nav .xpBar .xpBreakdown .list"
|
||||
) as HTMLElement;
|
||||
const levelEl = document.querySelector("nav .level") as HTMLElement;
|
||||
const barEl = document.querySelector("nav .xpBar .bar") as HTMLElement;
|
||||
const barWrapperEl = document.querySelector("nav .xpBar") as HTMLElement;
|
||||
|
||||
export async function skipBreakdown(): Promise<void> {
|
||||
skip = true;
|
||||
|
|
@ -33,13 +37,21 @@ export async function skipBreakdown(): Promise<void> {
|
|||
if (!breakdownDone) {
|
||||
void flashTotalXp(lastUpdate.addedXp, true);
|
||||
} else {
|
||||
xpBreakdownTotalEl.text(`+${lastUpdate.addedXp}`);
|
||||
xpBreakdownTotalEl.textContent = `+${lastUpdate.addedXp}`;
|
||||
}
|
||||
|
||||
xpBreakdownListEl.stop(true, true).empty().addClass("hidden");
|
||||
levelEl.text(
|
||||
Levels.getLevelFromTotalXp(lastUpdate.currentXp + lastUpdate.addedXp)
|
||||
);
|
||||
animate(xpBreakdownListEl, {
|
||||
opacity: [1, 0],
|
||||
duration: Misc.applyReducedMotion(250),
|
||||
onComplete: () => {
|
||||
xpBreakdownListEl.innerHTML = "";
|
||||
xpBreakdownListEl.classList.add("hidden");
|
||||
},
|
||||
});
|
||||
|
||||
levelEl.textContent = `${Levels.getLevelFromTotalXp(
|
||||
lastUpdate.currentXp + lastUpdate.addedXp
|
||||
)}`;
|
||||
|
||||
const endingDetails = Levels.getXpDetails(
|
||||
lastUpdate.currentXp + lastUpdate.addedXp
|
||||
|
|
@ -48,27 +60,21 @@ export async function skipBreakdown(): Promise<void> {
|
|||
endingDetails.level +
|
||||
endingDetails.levelCurrentXp / endingDetails.levelMaxXp;
|
||||
|
||||
barEl.css("width", `${(endingLevel % 1) * 100}%`);
|
||||
barEl.style.width = `${(endingLevel % 1) * 100}%`;
|
||||
await Misc.sleep(2000);
|
||||
breakdownVisible = false;
|
||||
barWrapperEl
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
SlowTimer.get() ? 0 : Misc.applyReducedMotion(250)
|
||||
);
|
||||
|
||||
animate(barWrapperEl, {
|
||||
opacity: [1, 0],
|
||||
duration: Misc.applyReducedMotion(250),
|
||||
});
|
||||
}
|
||||
|
||||
export function setXp(xp: number): void {
|
||||
const xpDetails = Levels.getXpDetails(xp);
|
||||
const levelCompletionRatio = xpDetails.levelCurrentXp / xpDetails.levelMaxXp;
|
||||
levelEl.text(xpDetails.level);
|
||||
barEl.css({
|
||||
width: levelCompletionRatio * 100 + "%",
|
||||
});
|
||||
levelEl.textContent = `${xpDetails.level}`;
|
||||
barEl.style.width = levelCompletionRatio * 100 + "%";
|
||||
}
|
||||
|
||||
export async function update(
|
||||
|
|
@ -84,7 +90,7 @@ export async function update(
|
|||
breakdown,
|
||||
};
|
||||
|
||||
levelEl.text(Levels.getLevelFromTotalXp(currentXp));
|
||||
levelEl.textContent = `${Levels.getLevelFromTotalXp(currentXp)}`;
|
||||
|
||||
const startingXp = Levels.getXpDetails(currentXp);
|
||||
const endingXp = Levels.getXpDetails(currentXp + addedXp);
|
||||
|
|
@ -93,35 +99,28 @@ export async function update(
|
|||
const endingLevel =
|
||||
endingXp.level + endingXp.levelCurrentXp / endingXp.levelMaxXp;
|
||||
|
||||
const breakdownList = xpBreakdownListEl;
|
||||
xpBreakdownListEl.style.opacity = "0";
|
||||
xpBreakdownListEl.innerHTML = "";
|
||||
barWrapperEl.style.opacity = "0";
|
||||
xpBreakdownTotalEl.textContent = "";
|
||||
|
||||
xpBreakdownListEl.stop(true, true).css("opacity", 0).empty();
|
||||
barWrapperEl.stop(true, true).css("opacity", 0);
|
||||
xpBreakdownTotalEl.text("");
|
||||
const showParent = Misc.promiseAnimate(barWrapperEl, {
|
||||
opacity: 1,
|
||||
duration: Misc.applyReducedMotion(125),
|
||||
ease: "linear",
|
||||
});
|
||||
|
||||
const showParent = Misc.promiseAnimation(
|
||||
barWrapperEl,
|
||||
{
|
||||
opacity: "1",
|
||||
},
|
||||
SlowTimer.get() ? 0 : Misc.applyReducedMotion(125),
|
||||
"linear"
|
||||
);
|
||||
|
||||
const showList = Misc.promiseAnimation(
|
||||
xpBreakdownListEl,
|
||||
{
|
||||
opacity: "1",
|
||||
},
|
||||
SlowTimer.get() ? 0 : Misc.applyReducedMotion(125),
|
||||
"linear"
|
||||
);
|
||||
const showList = Misc.promiseAnimate(xpBreakdownListEl, {
|
||||
opacity: 1,
|
||||
duration: Misc.applyReducedMotion(125),
|
||||
ease: "linear",
|
||||
});
|
||||
|
||||
if (breakdown !== undefined) {
|
||||
breakdownList.removeClass("hidden");
|
||||
xpBreakdownListEl.classList.remove("hidden");
|
||||
void Promise.all([showParent, showList]);
|
||||
} else {
|
||||
breakdownList.addClass("hidden");
|
||||
xpBreakdownListEl.classList.add("hidden");
|
||||
void showParent;
|
||||
}
|
||||
|
||||
|
|
@ -139,60 +138,28 @@ export async function update(
|
|||
if (skip) return;
|
||||
|
||||
breakdownVisible = false;
|
||||
levelEl.text(Levels.getLevelFromTotalXp(currentXp + addedXp));
|
||||
barWrapperEl
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
SlowTimer.get() ? 0 : Misc.applyReducedMotion(250)
|
||||
);
|
||||
levelEl.textContent = `${Levels.getLevelFromTotalXp(currentXp + addedXp)}`;
|
||||
|
||||
animate(barWrapperEl, {
|
||||
opacity: [1, 0],
|
||||
duration: Misc.applyReducedMotion(250),
|
||||
});
|
||||
}
|
||||
|
||||
async function flashTotalXp(totalXp: number, force = false): Promise<void> {
|
||||
if (!force && skip) return;
|
||||
|
||||
xpBreakdownTotalEl.text(`+${totalXp}`);
|
||||
xpBreakdownTotalEl.textContent = `+${totalXp}`;
|
||||
|
||||
const rand = (Math.random() * 2 - 1) / 4;
|
||||
const rand2 = (Math.random() + 1) / 2;
|
||||
|
||||
/**
|
||||
* `borderSpacing` has no visible effect on this element,
|
||||
* and is used in the animation only to provide numerical
|
||||
* values for the `step(step)` function.
|
||||
*/
|
||||
xpBreakdownTotalEl
|
||||
.stop(true, true)
|
||||
.css({
|
||||
transition: "initial",
|
||||
borderSpacing: 100,
|
||||
})
|
||||
.animate(
|
||||
{
|
||||
borderSpacing: 0,
|
||||
},
|
||||
{
|
||||
step(step) {
|
||||
xpBreakdownTotalEl.css(
|
||||
"transform",
|
||||
`scale(${1 + (step / 200) * rand2}) rotate(${
|
||||
(step / 10) * rand
|
||||
}deg)`
|
||||
);
|
||||
},
|
||||
duration: Misc.applyReducedMotion(2000),
|
||||
easing: "easeOutCubic",
|
||||
complete: () => {
|
||||
xpBreakdownTotalEl.css({
|
||||
backgroundColor: "",
|
||||
transition: "",
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
animate(xpBreakdownTotalEl, {
|
||||
scale: [1 + 0.5 * rand2, 1],
|
||||
rotate: [10 * rand, 0],
|
||||
duration: Misc.applyReducedMotion(2000),
|
||||
ease: "out(5)",
|
||||
});
|
||||
}
|
||||
|
||||
async function addBreakdownListItem(
|
||||
|
|
@ -203,11 +170,13 @@ async function addBreakdownListItem(
|
|||
if (skip) return;
|
||||
|
||||
if (amount === undefined) {
|
||||
xpBreakdownListEl.append(
|
||||
xpBreakdownListEl.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<div class="line" data-string='${string}'><div>${string}</div><div></div></div>`
|
||||
);
|
||||
} else if (typeof amount === "string") {
|
||||
xpBreakdownListEl.append(
|
||||
xpBreakdownListEl.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`
|
||||
<div class="line" data-string='${string}'>
|
||||
<div class="${options?.extraClass}">${string}</div>
|
||||
|
|
@ -217,29 +186,29 @@ async function addBreakdownListItem(
|
|||
} else {
|
||||
const positive = amount === undefined ? undefined : amount >= 0;
|
||||
|
||||
xpBreakdownListEl.append(`
|
||||
xpBreakdownListEl.insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`
|
||||
<div class="line" data-string='${string}'>
|
||||
|
||||
<div class="${options?.extraClass}">${string}</div>
|
||||
<div class="${positive ? "positive" : "negative"} ${
|
||||
options?.extraClass
|
||||
}">${positive ? "+" : "-"}${Math.abs(amount)}</div>
|
||||
</div>`);
|
||||
options?.extraClass
|
||||
}">${positive ? "+" : "-"}${Math.abs(amount)}</div>
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.noAnimation) return;
|
||||
|
||||
const el = xpBreakdownListEl.find(`.line[data-string='${string}']`);
|
||||
const el = xpBreakdownListEl.querySelector(
|
||||
`.line[data-string='${string}']`
|
||||
) as HTMLElement;
|
||||
|
||||
el.css("opacity", 0);
|
||||
await Misc.promiseAnimation(
|
||||
el,
|
||||
{
|
||||
opacity: "1",
|
||||
},
|
||||
Misc.applyReducedMotion(250),
|
||||
"swing"
|
||||
);
|
||||
await Misc.promiseAnimate(el, {
|
||||
opacity: [0, 1],
|
||||
duration: Misc.applyReducedMotion(250),
|
||||
});
|
||||
}
|
||||
|
||||
async function animateXpBreakdown(
|
||||
|
|
@ -248,20 +217,20 @@ async function animateXpBreakdown(
|
|||
): Promise<void> {
|
||||
if (skip) return;
|
||||
|
||||
xpBreakdownListEl.css("opacity", 1);
|
||||
xpBreakdownListEl.style.opacity = "1";
|
||||
if (!breakdown) {
|
||||
xpBreakdownTotalEl.text(`+${addedXp}`);
|
||||
xpBreakdownTotalEl.textContent = `+${addedXp}`;
|
||||
return;
|
||||
}
|
||||
const delay = Misc.applyReducedMotion(250);
|
||||
let total = 0;
|
||||
xpBreakdownListEl.empty();
|
||||
xpBreakdownListEl.removeClass("hidden");
|
||||
xpBreakdownListEl.innerHTML = "";
|
||||
xpBreakdownListEl.classList.remove("hidden");
|
||||
|
||||
xpBreakdownTotalEl.text("+0");
|
||||
xpBreakdownTotalEl.textContent = `+0`;
|
||||
|
||||
total += breakdown.base ?? 0;
|
||||
xpBreakdownTotalEl.text(`+${total}`);
|
||||
xpBreakdownTotalEl.textContent = `+${total}`;
|
||||
await addBreakdownListItem("time typing", breakdown.base, {
|
||||
noAnimation: true,
|
||||
});
|
||||
|
|
@ -374,29 +343,27 @@ async function animateXpBar(
|
|||
|
||||
const difference = endingLevel - startingLevel;
|
||||
|
||||
barEl.css("width", `${(startingLevel % 1) * 100}%`);
|
||||
barEl.style.width = `${(startingLevel % 1) * 100}%`;
|
||||
|
||||
if (endingLevel % 1 === 0) {
|
||||
await Misc.promiseAnimation(
|
||||
barEl,
|
||||
{
|
||||
width: "100%",
|
||||
},
|
||||
SlowTimer.get() ? 0 : Misc.applyReducedMotion(1000),
|
||||
"easeOutExpo"
|
||||
);
|
||||
//ending level is exactly round, meaning fill the bar to 100%, flash, set to 0
|
||||
await Misc.promiseAnimate(barEl, {
|
||||
width: "100%",
|
||||
duration: Misc.applyReducedMotion(1000),
|
||||
ease: "out(5)",
|
||||
});
|
||||
|
||||
if (skip) return;
|
||||
|
||||
void flashLevel();
|
||||
barEl.css("width", `0%`);
|
||||
barEl.style.width = `0%`;
|
||||
} else if (Math.floor(startingLevel) === Math.floor(endingLevel)) {
|
||||
await Misc.promiseAnimation(
|
||||
barEl,
|
||||
{ width: `${(endingLevel % 1) * 100}%` },
|
||||
SlowTimer.get() ? 0 : Misc.applyReducedMotion(1000),
|
||||
"easeOutExpo"
|
||||
);
|
||||
//ending level is the same, just animate the bar to the correct percentage
|
||||
await Misc.promiseAnimate(barEl, {
|
||||
width: `${(endingLevel % 1) * 100}%`,
|
||||
duration: Misc.applyReducedMotion(1000),
|
||||
ease: "out(5)",
|
||||
});
|
||||
} else {
|
||||
// const quickSpeed = Misc.mapRange(difference, 10, 2000, 200, 1);
|
||||
const quickSpeed = Math.min(1000 / difference, 200);
|
||||
|
|
@ -404,29 +371,23 @@ async function animateXpBar(
|
|||
|
||||
let firstOneDone = false;
|
||||
let animationDuration = quickSpeed;
|
||||
let animationEasing: Misc.JQueryEasing = "linear";
|
||||
let decrement = 1 - (startingLevel % 1);
|
||||
|
||||
do {
|
||||
if (skip) return;
|
||||
|
||||
if (toAnimate - 1 < 1) {
|
||||
animationDuration = mapRange(toAnimate - 1, 0, 0.5, 1000, 200);
|
||||
animationEasing = "easeOutQuad";
|
||||
}
|
||||
if (firstOneDone) {
|
||||
void flashLevel();
|
||||
barEl.css("width", "0%");
|
||||
barEl.style.width = "0%";
|
||||
decrement = 1;
|
||||
}
|
||||
await Misc.promiseAnimation(
|
||||
barEl,
|
||||
{
|
||||
width: "100%",
|
||||
},
|
||||
SlowTimer.get() ? 0 : Misc.applyReducedMotion(animationDuration),
|
||||
animationEasing
|
||||
);
|
||||
|
||||
await Misc.promiseAnimate(barEl, {
|
||||
width: "100%",
|
||||
duration: Misc.applyReducedMotion(animationDuration),
|
||||
ease: "linear",
|
||||
});
|
||||
|
||||
toAnimate -= decrement;
|
||||
firstOneDone = true;
|
||||
} while (toAnimate > 1);
|
||||
|
|
@ -434,18 +395,15 @@ async function animateXpBar(
|
|||
if (skip) return;
|
||||
|
||||
void flashLevel();
|
||||
barEl.css("width", "0%");
|
||||
barEl.style.width = "0%";
|
||||
|
||||
if (skip) return;
|
||||
|
||||
await Misc.promiseAnimation(
|
||||
barEl,
|
||||
{
|
||||
width: `${(toAnimate % 1) * 100}%`,
|
||||
},
|
||||
SlowTimer.get() ? 0 : Misc.applyReducedMotion(1000),
|
||||
"easeOutExpo"
|
||||
);
|
||||
await Misc.promiseAnimate(barEl, {
|
||||
width: `${(toAnimate % 1) * 100}%`,
|
||||
duration: Misc.applyReducedMotion(1000),
|
||||
ease: "out(5)",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -453,7 +411,7 @@ async function animateXpBar(
|
|||
async function flashLevel(): Promise<void> {
|
||||
const themecolors = await getAll();
|
||||
|
||||
levelEl.text(parseInt(levelEl.text()) + 1);
|
||||
levelEl.textContent = `${parseInt(levelEl.textContent ?? "0") + 1}`;
|
||||
|
||||
const rand = Math.random() * 2 - 1;
|
||||
const rand2 = Math.random() + 1;
|
||||
|
|
@ -463,36 +421,12 @@ async function flashLevel(): Promise<void> {
|
|||
* and is used in the animation only to provide numerical
|
||||
* values for the `step(step)` function.
|
||||
*/
|
||||
levelEl
|
||||
.stop(true, true)
|
||||
.css({
|
||||
backgroundColor: themecolors.main,
|
||||
// transform: "scale(1.5) rotate(10deg)",
|
||||
borderSpacing: 100,
|
||||
transition: "initial",
|
||||
})
|
||||
.animate(
|
||||
{
|
||||
backgroundColor: themecolors.sub,
|
||||
borderSpacing: 0,
|
||||
},
|
||||
{
|
||||
step(step) {
|
||||
levelEl.css(
|
||||
"transform",
|
||||
`scale(${1 + (step / 200) * rand2}) rotate(${
|
||||
(step / 10) * rand
|
||||
}deg)`
|
||||
);
|
||||
},
|
||||
duration: Misc.applyReducedMotion(2000),
|
||||
easing: "easeOutCubic",
|
||||
complete: () => {
|
||||
levelEl.css({
|
||||
backgroundColor: "",
|
||||
transition: "",
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
animate(levelEl, {
|
||||
scale: [1 + 0.5 * rand2, 1],
|
||||
backgroundColor: [themecolors.main, themecolors.sub],
|
||||
rotate: [10 * rand, 0],
|
||||
duration: Misc.applyReducedMotion(2000),
|
||||
ease: "out(5)",
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
// this file should be concatenated at the top of the legacy ts files
|
||||
import "jquery-color";
|
||||
import "jquery.easing";
|
||||
|
||||
import "./event-handlers/global";
|
||||
import "./event-handlers/footer";
|
||||
import "./event-handlers/keymap";
|
||||
|
|
@ -51,6 +47,7 @@ import * as Cookies from "./cookies";
|
|||
import "./elements/psa";
|
||||
import "./utils/url-handler";
|
||||
import "./modals/last-signed-out-result";
|
||||
import { applyEngineSettings } from "./anim";
|
||||
|
||||
// Lock Math.random
|
||||
Object.defineProperty(Math, "random", {
|
||||
|
|
@ -71,6 +68,7 @@ Object.defineProperty(window, "Math", {
|
|||
enumerable: true,
|
||||
});
|
||||
|
||||
applyEngineSettings();
|
||||
void loadFromLocalStorage();
|
||||
void VersionButton.update();
|
||||
Focus.set(true, true);
|
||||
|
|
|
|||
|
|
@ -133,8 +133,8 @@ function updateIntegrationSections(): void {
|
|||
|
||||
function updateTabs(): void {
|
||||
void swapElements(
|
||||
pageElement.find(".tab.active"),
|
||||
pageElement.find(`.tab[data-tab="${state.tab}"]`),
|
||||
pageElement.find(".tab.active")[0] as HTMLElement,
|
||||
pageElement.find(`.tab[data-tab="${state.tab}"]`)[0] as HTMLElement,
|
||||
250,
|
||||
async () => {
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1070,7 +1070,6 @@ $(".pageAccount #accountHistoryChart").on("click", () => {
|
|||
const index: number = ChartController.accountHistoryActiveIndex;
|
||||
loadMoreLines(index);
|
||||
if (window === undefined) return;
|
||||
const windowHeight = $(window).height() ?? 0;
|
||||
|
||||
const resultId = filteredResults[index]?._id;
|
||||
if (resultId === undefined) {
|
||||
|
|
@ -1079,20 +1078,11 @@ $(".pageAccount #accountHistoryChart").on("click", () => {
|
|||
const element = $(`.resultRow[data-id="${resultId}"`);
|
||||
$(".resultRow").removeClass("active");
|
||||
|
||||
const offset = element.offset()?.top ?? 0;
|
||||
const scrollTo = offset - windowHeight / 2;
|
||||
$([document.documentElement, document.body])
|
||||
.stop(true)
|
||||
.animate(
|
||||
{ scrollTop: scrollTo },
|
||||
{
|
||||
duration: Misc.applyReducedMotion(500),
|
||||
done: () => {
|
||||
$(".resultRow").removeClass("active");
|
||||
requestAnimationFrame(() => element.addClass("active"));
|
||||
},
|
||||
}
|
||||
);
|
||||
element[0]?.scrollIntoView({
|
||||
block: "center",
|
||||
});
|
||||
|
||||
element.addClass("active");
|
||||
});
|
||||
|
||||
$(".pageAccount").on("click", ".miniResultChartButton", async (event) => {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { differenceInSeconds } from "date-fns/differenceInSeconds";
|
|||
import * as DateTime from "../utils/date-and-time";
|
||||
import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
|
||||
import { getHTMLById as getBadgeHTMLbyId } from "../controllers/badge-controller";
|
||||
import { applyReducedMotion, isDevEnvironment } from "../utils/misc";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { abbreviateNumber } from "../utils/numbers";
|
||||
import { formatDistanceToNow } from "date-fns/formatDistanceToNow";
|
||||
import { z } from "zod";
|
||||
|
|
@ -898,15 +898,9 @@ function updateContent(): void {
|
|||
}
|
||||
|
||||
if (state.scrollToUserAfterFill) {
|
||||
const windowHeight = $(window).height() ?? 0;
|
||||
const offset = $(`.tableAndUser .me`).offset()?.top ?? 0;
|
||||
const scrollTo = offset - windowHeight / 2;
|
||||
$([document.documentElement, document.body]).animate(
|
||||
{
|
||||
scrollTop: scrollTo,
|
||||
},
|
||||
applyReducedMotion(500)
|
||||
);
|
||||
document.querySelector(".tableAndUser .me")?.scrollIntoView({
|
||||
block: "center",
|
||||
});
|
||||
state.scrollToUserAfterFill = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Page from "./page";
|
||||
import * as Skeleton from "../utils/skeleton";
|
||||
import { promiseAnimate } from "../utils/misc";
|
||||
|
||||
const pageEl = $(".page.pageLoading");
|
||||
const barEl = pageEl.find(".bar");
|
||||
|
|
@ -11,19 +12,9 @@ export async function updateBar(
|
|||
percentage: number,
|
||||
duration: number
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
barEl
|
||||
.find(".fill")
|
||||
.stop(true, false)
|
||||
.animate(
|
||||
{
|
||||
width: percentage + "%",
|
||||
},
|
||||
duration,
|
||||
() => {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
await promiseAnimate(barEl[0]?.querySelector(".fill") as HTMLElement, {
|
||||
width: percentage + "%",
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import { Fonts } from "../constants/fonts";
|
|||
import * as CustomBackgroundPicker from "../elements/settings/custom-background-picker";
|
||||
import * as CustomFontPicker from "../elements/settings/custom-font-picker";
|
||||
import * as AuthEvent from "../observables/auth-event";
|
||||
import * as FpsLimitSection from "../elements/settings/fps-limit-section";
|
||||
|
||||
let settingsInitialized = false;
|
||||
|
||||
|
|
@ -857,6 +858,7 @@ export async function update(
|
|||
await CustomBackgroundPicker.updateUI();
|
||||
await updateFilterSectionVisibility();
|
||||
await CustomFontPicker.updateUI();
|
||||
FpsLimitSection.update();
|
||||
|
||||
const setInputValue = (
|
||||
key: ConfigKey,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import * as Notifications from "../elements/notifications";
|
|||
import * as AdController from "../controllers/ad-controller";
|
||||
import * as Skeleton from "../utils/skeleton";
|
||||
import { isPopupVisible } from "../utils/misc";
|
||||
import { animate } from "animejs";
|
||||
|
||||
const wrapperId = "videoAdPopupWrapper";
|
||||
|
||||
|
|
@ -34,32 +35,33 @@ export async function show(): Promise<void> {
|
|||
}
|
||||
|
||||
if (!isPopupVisible(wrapperId)) {
|
||||
$("#videoAdPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 0)
|
||||
.removeClass("hidden")
|
||||
.animate({ opacity: 1 }, 125, () => {
|
||||
const el = document.querySelector("#videoAdPopupWrapper") as HTMLElement;
|
||||
|
||||
animate(el, {
|
||||
opacity: [0, 1],
|
||||
duration: 125,
|
||||
onBegin: () => {
|
||||
el.classList.remove("hidden");
|
||||
},
|
||||
onComplete: () => {
|
||||
//@ts-expect-error 3rd party ad code
|
||||
window.dataLayer.push({ event: "EG_Video" });
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hide(): void {
|
||||
if (isPopupVisible(wrapperId)) {
|
||||
$("#videoAdPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
125,
|
||||
() => {
|
||||
$("#videoAdPopupWrapper").addClass("hidden");
|
||||
Skeleton.remove(wrapperId);
|
||||
}
|
||||
);
|
||||
const el = document.querySelector("#videoAdPopupWrapper") as HTMLElement;
|
||||
animate(el, {
|
||||
opacity: [1, 0],
|
||||
duration: 125,
|
||||
onComplete: () => {
|
||||
el.classList.add("hidden");
|
||||
Skeleton.remove(wrapperId);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import * as ServerConfiguration from "./ape/server-configuration";
|
|||
import { getActiveFunboxesWithFunction } from "./test/funbox/list";
|
||||
import { loadPromise } from "./config";
|
||||
import { authPromise } from "./firebase";
|
||||
import { animate } from "animejs";
|
||||
|
||||
$(async (): Promise<void> => {
|
||||
await loadPromise;
|
||||
|
|
@ -23,11 +24,12 @@ $(async (): Promise<void> => {
|
|||
fb.functions.applyGlobalCSS();
|
||||
}
|
||||
|
||||
$("#app")
|
||||
.css("opacity", "0")
|
||||
.removeClass("hidden")
|
||||
.stop(true, true)
|
||||
.animate({ opacity: 1 }, Misc.applyReducedMotion(250));
|
||||
const app = document.querySelector("#app") as HTMLElement;
|
||||
app?.classList.remove("hidden");
|
||||
animate(app, {
|
||||
opacity: [0, 1],
|
||||
duration: Misc.applyReducedMotion(250),
|
||||
});
|
||||
if (ConnectionState.get()) {
|
||||
void ServerConfiguration.sync().then(() => {
|
||||
if (!ServerConfiguration.get()?.users.signUp) {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ export function hide(): void {
|
|||
}
|
||||
|
||||
export function resetPosition(): void {
|
||||
caret.clearMargins();
|
||||
caret.stopAllAnimations();
|
||||
caret.clearMargins();
|
||||
caret.goTo({
|
||||
wordIndex: 0,
|
||||
letterIndex: 0,
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import { animate } from "animejs";
|
||||
import { capitalizeFirstLetter } from "../../utils/strings";
|
||||
import { applyReducedMotion } from "../../utils/misc";
|
||||
|
||||
const timerEl = document.querySelector(
|
||||
"#typingTest #layoutfluidTimer"
|
||||
) as HTMLElement;
|
||||
|
||||
export function show(): void {
|
||||
$("#typingTest #layoutfluidTimer").stop(true, true).animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
125
|
||||
);
|
||||
animate(timerEl, {
|
||||
opacity: 1,
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
}
|
||||
|
||||
export function hide(): void {
|
||||
$("#typingTest #layoutfluidTimer").stop(true, true).animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
125
|
||||
);
|
||||
animate(timerEl, {
|
||||
opacity: 0,
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
}
|
||||
|
||||
export function updateTime(sec: number, layout: string): void {
|
||||
$("#typingTest #layoutfluidTimer").text(
|
||||
`${capitalizeFirstLetter(layout)} in: ${sec}s`
|
||||
);
|
||||
timerEl.textContent = `${capitalizeFirstLetter(layout)} in: ${sec}s`;
|
||||
}
|
||||
|
||||
export function updateWords(words: number, layout: string): void {
|
||||
|
|
@ -30,5 +30,5 @@ export function updateWords(words: number, layout: string): void {
|
|||
if (words === 1) {
|
||||
str = `${layoutName} starting next word`;
|
||||
}
|
||||
$("#typingTest #layoutfluidTimer").text(str);
|
||||
timerEl.textContent = str;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import { animate } from "animejs";
|
||||
import { applyReducedMotion } from "../../utils/misc";
|
||||
|
||||
let memoryTimer: number | null = null;
|
||||
let memoryInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const timerEl = document.querySelector(
|
||||
"#typingTest #memoryTimer"
|
||||
) as HTMLElement;
|
||||
|
||||
export function show(): void {
|
||||
$("#typingTest #memoryTimer").stop(true, true).animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
125
|
||||
);
|
||||
animate(timerEl, {
|
||||
opacity: 1,
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
}
|
||||
|
||||
export function hide(): void {
|
||||
$("#typingTest #memoryTimer").stop(true, true).animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
125
|
||||
);
|
||||
animate(timerEl, {
|
||||
opacity: 0,
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
}
|
||||
|
||||
export function reset(): void {
|
||||
|
|
@ -45,7 +48,5 @@ export function start(time: number): void {
|
|||
}
|
||||
|
||||
export function update(sec: number): void {
|
||||
$("#typingTest #memoryTimer").text(
|
||||
`Timer left to memorise all words: ${sec}s`
|
||||
);
|
||||
timerEl.textContent = `Timer left to memorise all words: ${sec}s`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Config from "../config";
|
|||
import * as TestState from "../test/test-state";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
import { applyReducedMotion } from "../utils/misc";
|
||||
import { animate } from "animejs";
|
||||
|
||||
const textEl = document.querySelector(
|
||||
"#liveStatsTextBottom .liveAcc"
|
||||
|
|
@ -29,47 +30,37 @@ export function show(): void {
|
|||
if (!TestState.isActive) return;
|
||||
if (state) return;
|
||||
if (Config.liveAccStyle === "mini") {
|
||||
$(miniEl).stop(true, false).removeClass("hidden").css("opacity", 0).animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
miniEl.classList.remove("hidden");
|
||||
animate(miniEl, {
|
||||
opacity: [0, 1],
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
} else {
|
||||
$(textEl).stop(true, false).removeClass("hidden").css("opacity", 0).animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
textEl.classList.remove("hidden");
|
||||
animate(textEl, {
|
||||
opacity: [0, 1],
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
}
|
||||
state = true;
|
||||
}
|
||||
|
||||
export function hide(): void {
|
||||
if (!state) return;
|
||||
$(textEl)
|
||||
.stop(true, false)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
applyReducedMotion(125),
|
||||
() => {
|
||||
$(textEl).addClass("hidden");
|
||||
}
|
||||
);
|
||||
$(miniEl)
|
||||
.stop(true, false)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
applyReducedMotion(125),
|
||||
() => {
|
||||
$(miniEl).addClass("hidden");
|
||||
}
|
||||
);
|
||||
animate(textEl, {
|
||||
opacity: [1, 0],
|
||||
duration: applyReducedMotion(125),
|
||||
onComplete: () => {
|
||||
textEl.classList.add("hidden");
|
||||
},
|
||||
});
|
||||
animate(miniEl, {
|
||||
opacity: [1, 0],
|
||||
duration: applyReducedMotion(125),
|
||||
onComplete: () => {
|
||||
miniEl.classList.add("hidden");
|
||||
},
|
||||
});
|
||||
state = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import * as TestState from "../test/test-state";
|
|||
import * as ConfigEvent from "../observables/config-event";
|
||||
import Format from "../utils/format";
|
||||
import { applyReducedMotion } from "../utils/misc";
|
||||
import { animate } from "animejs";
|
||||
|
||||
const textEl = document.querySelector(
|
||||
"#liveStatsTextBottom .liveBurst"
|
||||
|
|
@ -27,47 +28,37 @@ export function show(): void {
|
|||
if (!TestState.isActive) return;
|
||||
if (state) return;
|
||||
if (Config.liveBurstStyle === "mini") {
|
||||
$(miniEl).stop(true, false).removeClass("hidden").css("opacity", 0).animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
miniEl.classList.remove("hidden");
|
||||
animate(miniEl, {
|
||||
opacity: [0, 1],
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
} else {
|
||||
$(textEl).stop(true, false).removeClass("hidden").css("opacity", 0).animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
textEl.classList.remove("hidden");
|
||||
animate(textEl, {
|
||||
opacity: [0, 1],
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
}
|
||||
state = true;
|
||||
}
|
||||
|
||||
export function hide(): void {
|
||||
if (!state) return;
|
||||
$(textEl)
|
||||
.stop(true, false)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
applyReducedMotion(125),
|
||||
() => {
|
||||
$(textEl).addClass("hidden");
|
||||
}
|
||||
);
|
||||
$(miniEl)
|
||||
.stop(true, false)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
applyReducedMotion(125),
|
||||
() => {
|
||||
$(miniEl).addClass("hidden");
|
||||
}
|
||||
);
|
||||
animate(textEl, {
|
||||
opacity: [1, 0],
|
||||
duration: applyReducedMotion(125),
|
||||
onComplete: () => {
|
||||
textEl.classList.add("hidden");
|
||||
},
|
||||
});
|
||||
animate(miniEl, {
|
||||
opacity: [1, 0],
|
||||
duration: applyReducedMotion(125),
|
||||
onComplete: () => {
|
||||
miniEl.classList.add("hidden");
|
||||
},
|
||||
});
|
||||
state = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import * as TestState from "./test-state";
|
|||
import * as ConfigEvent from "../observables/config-event";
|
||||
import Format from "../utils/format";
|
||||
import { applyReducedMotion } from "../utils/misc";
|
||||
import { animate } from "animejs";
|
||||
|
||||
const textElement = document.querySelector(
|
||||
"#liveStatsTextBottom .liveSpeed"
|
||||
|
|
@ -31,55 +32,37 @@ export function show(): void {
|
|||
if (!TestState.isActive) return;
|
||||
if (state) return;
|
||||
if (Config.liveSpeedStyle === "mini") {
|
||||
$(miniElement)
|
||||
.stop(true, false)
|
||||
.removeClass("hidden")
|
||||
.css("opacity", 0)
|
||||
.animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
miniElement.classList.remove("hidden");
|
||||
animate(miniElement, {
|
||||
opacity: [0, 1],
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
} else {
|
||||
$(textElement)
|
||||
.stop(true, false)
|
||||
.removeClass("hidden")
|
||||
.css("opacity", 0)
|
||||
.animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
textElement.classList.remove("hidden");
|
||||
animate(textElement, {
|
||||
opacity: [0, 1],
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
}
|
||||
state = true;
|
||||
}
|
||||
|
||||
export function hide(): void {
|
||||
if (!state) return;
|
||||
$(textElement)
|
||||
.stop(true, false)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
applyReducedMotion(125),
|
||||
() => {
|
||||
textElement.classList.add("hidden");
|
||||
}
|
||||
);
|
||||
$(miniElement)
|
||||
.stop(true, false)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
applyReducedMotion(125),
|
||||
() => {
|
||||
miniElement.classList.add("hidden");
|
||||
}
|
||||
);
|
||||
animate(miniElement, {
|
||||
opacity: [1, 0],
|
||||
duration: applyReducedMotion(125),
|
||||
onComplete: () => {
|
||||
miniElement.classList.add("hidden");
|
||||
},
|
||||
});
|
||||
animate(textElement, {
|
||||
opacity: [1, 0],
|
||||
duration: applyReducedMotion(125),
|
||||
onComplete: () => {
|
||||
textElement.classList.add("hidden");
|
||||
},
|
||||
});
|
||||
state = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Config from "../config";
|
|||
import * as ConfigEvent from "../observables/config-event";
|
||||
import * as TestState from "../test/test-state";
|
||||
import * as KeyConverter from "../utils/key-converter";
|
||||
import { animate } from "animejs";
|
||||
|
||||
ConfigEvent.subscribe((eventKey) => {
|
||||
if (eventKey === "monkey" && TestState.isActive) {
|
||||
|
|
@ -64,7 +65,10 @@ function update(): void {
|
|||
export function updateFastOpacity(num: number): void {
|
||||
if (!Config.monkey) return;
|
||||
const opacity = mapRange(num, 130, 180, 0, 1);
|
||||
$("#monkey .fast").animate({ opacity: opacity }, 1000);
|
||||
animate("#monkey .fast", {
|
||||
opacity: opacity,
|
||||
duration: 1000,
|
||||
});
|
||||
let animDuration = mapRange(num, 130, 180, 0.25, 0.01);
|
||||
if (animDuration === 0.25) animDuration = 0;
|
||||
$("#monkey").css({ animationDuration: animDuration + "s" });
|
||||
|
|
@ -137,18 +141,20 @@ export function stop(event: JQuery.KeyUpEvent): void {
|
|||
|
||||
export function show(): void {
|
||||
if (!Config.monkey) return;
|
||||
$("#monkey")
|
||||
.css("opacity", 0)
|
||||
.removeClass("hidden")
|
||||
.animate({ opacity: 1 }, 125);
|
||||
$("#monkey").removeClass("hidden");
|
||||
animate("#monkey", {
|
||||
opacity: [0, 1],
|
||||
duration: 125,
|
||||
});
|
||||
}
|
||||
|
||||
export function hide(): void {
|
||||
$("#monkey")
|
||||
.css("opacity", 1)
|
||||
.animate({ opacity: 1 }, 125, () => {
|
||||
$("#monkey").addClass("hidden");
|
||||
$("#monkey .fast").stop(true, true).css("opacity", 0);
|
||||
$("#monkey").stop(true, true).css({ animationDuration: "0s" });
|
||||
});
|
||||
animate("#monkey", {
|
||||
opacity: [1, 0],
|
||||
duration: 125,
|
||||
onComplete: () => {
|
||||
$("#monkey").addClass("hidden").css({ animationDuration: "0s" });
|
||||
$("#monkey .fast").css("opacity", 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { animate } from "animejs";
|
||||
import { applyReducedMotion } from "../utils/misc";
|
||||
|
||||
export function hide(): void {
|
||||
|
|
@ -22,14 +23,17 @@ export function getCurrentType(): CrownType {
|
|||
export function show(): void {
|
||||
if (visible) return;
|
||||
visible = true;
|
||||
const el = $("#result .stats .wpm .crown");
|
||||
el.removeClass("hidden").css("opacity", "0").animate(
|
||||
{
|
||||
opacity: 1,
|
||||
const el = document.querySelector(
|
||||
"#result .stats .wpm .crown"
|
||||
) as HTMLElement;
|
||||
|
||||
animate(el, {
|
||||
opacity: [0, 1],
|
||||
duration: applyReducedMotion(125),
|
||||
onBegin: () => {
|
||||
el.classList.remove("hidden");
|
||||
},
|
||||
applyReducedMotion(250),
|
||||
"easeOutCubic"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function update(type: CrownType): void {
|
||||
|
|
|
|||
|
|
@ -1072,8 +1072,8 @@ export async function update(
|
|||
TestConfig.hide();
|
||||
|
||||
void Misc.swapElements(
|
||||
$("#typingTest"),
|
||||
$("#result"),
|
||||
document.querySelector("#typingTest") as HTMLElement,
|
||||
document.querySelector("#result") as HTMLElement,
|
||||
250,
|
||||
async () => {
|
||||
const result = document.querySelector<HTMLElement>("#result");
|
||||
|
|
@ -1088,12 +1088,6 @@ export async function update(
|
|||
},
|
||||
async () => {
|
||||
Focus.set(false);
|
||||
$("#resultExtraButtons").removeClass("hidden").css("opacity", 0).animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
Misc.applyReducedMotion(125)
|
||||
);
|
||||
|
||||
const canQuickRestart = canQuickRestartFn(
|
||||
Config.mode,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import { Mode } from "@monkeytype/schemas/shared";
|
|||
import Config from "../config";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
import * as ActivePage from "../states/active-page";
|
||||
import { applyReducedMotion } from "../utils/misc";
|
||||
import { applyReducedMotion, promiseAnimate } from "../utils/misc";
|
||||
import { areUnsortedArraysEqual } from "../utils/arrays";
|
||||
import * as AuthEvent from "../observables/auth-event";
|
||||
import { animate } from "animejs";
|
||||
|
||||
export function show(): void {
|
||||
$("#testConfig").removeClass("invisible");
|
||||
|
|
@ -24,7 +25,7 @@ export async function instantUpdate(): Promise<void> {
|
|||
);
|
||||
|
||||
$("#testConfig .puncAndNum").addClass("hidden");
|
||||
$("#testConfig .spacer").css("transition", "none").addClass("scrolled");
|
||||
$("#testConfig .spacer").addClass("hidden");
|
||||
$("#testConfig .time").addClass("hidden");
|
||||
$("#testConfig .wordCount").addClass("hidden");
|
||||
$("#testConfig .customText").addClass("hidden");
|
||||
|
|
@ -36,8 +37,8 @@ export async function instantUpdate(): Promise<void> {
|
|||
width: "",
|
||||
opacity: "",
|
||||
});
|
||||
$("#testConfig .leftSpacer").removeClass("scrolled");
|
||||
$("#testConfig .rightSpacer").removeClass("scrolled");
|
||||
$("#testConfig .leftSpacer").removeClass("hidden");
|
||||
$("#testConfig .rightSpacer").removeClass("hidden");
|
||||
$("#testConfig .time").removeClass("hidden");
|
||||
|
||||
updateActiveExtraButtons("time", Config.time);
|
||||
|
|
@ -46,13 +47,13 @@ export async function instantUpdate(): Promise<void> {
|
|||
width: "",
|
||||
opacity: "",
|
||||
});
|
||||
$("#testConfig .leftSpacer").removeClass("scrolled");
|
||||
$("#testConfig .rightSpacer").removeClass("scrolled");
|
||||
$("#testConfig .leftSpacer").removeClass("hidden");
|
||||
$("#testConfig .rightSpacer").removeClass("hidden");
|
||||
$("#testConfig .wordCount").removeClass("hidden");
|
||||
|
||||
updateActiveExtraButtons("words", Config.words);
|
||||
} else if (Config.mode === "quote") {
|
||||
$("#testConfig .rightSpacer").removeClass("scrolled");
|
||||
$("#testConfig .rightSpacer").removeClass("hidden");
|
||||
$("#testConfig .quoteLength").removeClass("hidden");
|
||||
|
||||
updateActiveExtraButtons("quoteLength", Config.quoteLength);
|
||||
|
|
@ -61,18 +62,14 @@ export async function instantUpdate(): Promise<void> {
|
|||
width: "",
|
||||
opacity: "",
|
||||
});
|
||||
$("#testConfig .leftSpacer").removeClass("scrolled");
|
||||
$("#testConfig .rightSpacer").removeClass("scrolled");
|
||||
$("#testConfig .leftSpacer").removeClass("hidden");
|
||||
$("#testConfig .rightSpacer").removeClass("hidden");
|
||||
$("#testConfig .customText").removeClass("hidden");
|
||||
}
|
||||
|
||||
updateActiveExtraButtons("quoteLength", Config.quoteLength);
|
||||
updateActiveExtraButtons("numbers", Config.numbers);
|
||||
updateActiveExtraButtons("punctuation", Config.punctuation);
|
||||
|
||||
setTimeout(() => {
|
||||
$("#testConfig .spacer").css("transition", "");
|
||||
}, 125);
|
||||
}
|
||||
|
||||
async function update(previous: Mode, current: Mode): Promise<void> {
|
||||
|
|
@ -100,10 +97,12 @@ async function update(previous: Mode, current: Mode): Promise<void> {
|
|||
};
|
||||
|
||||
const animTime = applyReducedMotion(250);
|
||||
|
||||
const scale = 2;
|
||||
const easing = {
|
||||
both: "easeInOutSine",
|
||||
in: "easeInSine",
|
||||
out: "easeOutSine",
|
||||
both: `inOut(${scale})`,
|
||||
in: `in(${scale})`,
|
||||
out: `out(${scale})`,
|
||||
};
|
||||
|
||||
const puncAndNumVisible = {
|
||||
|
|
@ -117,12 +116,6 @@ async function update(previous: Mode, current: Mode): Promise<void> {
|
|||
const puncAndNumEl = $("#testConfig .puncAndNum");
|
||||
|
||||
if (puncAndNumVisible[current] !== puncAndNumVisible[previous]) {
|
||||
if (!puncAndNumVisible[current]) {
|
||||
$("#testConfig .leftSpacer").addClass("scrolled");
|
||||
} else {
|
||||
$("#testConfig .leftSpacer").removeClass("scrolled");
|
||||
}
|
||||
|
||||
puncAndNumEl
|
||||
.css({
|
||||
width: "unset",
|
||||
|
|
@ -134,34 +127,87 @@ async function update(previous: Mode, current: Mode): Promise<void> {
|
|||
puncAndNumEl[0]?.getBoundingClientRect().width ?? 0
|
||||
);
|
||||
|
||||
puncAndNumEl
|
||||
.stop(true, false)
|
||||
.css({
|
||||
width: puncAndNumVisible[previous] ? width : 0,
|
||||
opacity: puncAndNumVisible[previous] ? 1 : 0,
|
||||
})
|
||||
.animate(
|
||||
{
|
||||
width: puncAndNumVisible[current] ? width : 0,
|
||||
opacity: puncAndNumVisible[current] ? 1 : 0,
|
||||
},
|
||||
animTime,
|
||||
easing.both,
|
||||
() => {
|
||||
if (puncAndNumVisible[current]) {
|
||||
puncAndNumEl.css("width", "unset");
|
||||
} else {
|
||||
puncAndNumEl.addClass("hidden");
|
||||
}
|
||||
animate(puncAndNumEl[0] as HTMLElement, {
|
||||
width: [
|
||||
(puncAndNumVisible[previous] ? width : 0) + "px",
|
||||
(puncAndNumVisible[current] ? width : 0) + "px",
|
||||
],
|
||||
opacity: {
|
||||
duration: animTime / 2,
|
||||
delay: puncAndNumVisible[current] ? animTime / 2 : 0,
|
||||
from: puncAndNumVisible[previous] ? 1 : 0,
|
||||
to: puncAndNumVisible[current] ? 1 : 0,
|
||||
},
|
||||
duration: animTime,
|
||||
ease: easing.both,
|
||||
onComplete: () => {
|
||||
if (puncAndNumVisible[current]) {
|
||||
puncAndNumEl.css("width", "unset");
|
||||
} else {
|
||||
puncAndNumEl.addClass("hidden");
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const leftSpacerEl = document.querySelector(
|
||||
"#testConfig .leftSpacer"
|
||||
) as HTMLElement;
|
||||
|
||||
leftSpacerEl.style.width = "0.5em";
|
||||
leftSpacerEl.style.opacity = "1";
|
||||
leftSpacerEl.classList.remove("hidden");
|
||||
|
||||
animate(leftSpacerEl, {
|
||||
width: [
|
||||
puncAndNumVisible[previous] ? "0.5em" : 0,
|
||||
puncAndNumVisible[current] ? "0.5em" : 0,
|
||||
],
|
||||
// opacity: {
|
||||
// duration: animTime / 2,
|
||||
// // delay: puncAndNumVisible[current] ? animTime / 2 : 0,
|
||||
// from: puncAndNumVisible[previous] ? 1 : 0,
|
||||
// to: puncAndNumVisible[current] ? 1 : 0,
|
||||
// },
|
||||
duration: animTime,
|
||||
ease: easing.both,
|
||||
onComplete: () => {
|
||||
if (puncAndNumVisible[current]) {
|
||||
leftSpacerEl.style.width = "";
|
||||
} else {
|
||||
leftSpacerEl.classList.add("hidden");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (current === "zen") {
|
||||
$("#testConfig .rightSpacer").addClass("scrolled");
|
||||
} else {
|
||||
$("#testConfig .rightSpacer").removeClass("scrolled");
|
||||
}
|
||||
const rightSpacerEl = document.querySelector(
|
||||
"#testConfig .rightSpacer"
|
||||
) as HTMLElement;
|
||||
|
||||
rightSpacerEl.style.width = "0.5em";
|
||||
rightSpacerEl.style.opacity = "1";
|
||||
rightSpacerEl.classList.remove("hidden");
|
||||
|
||||
animate(rightSpacerEl, {
|
||||
width: [
|
||||
previous === "zen" ? "0px" : "0.5em",
|
||||
current === "zen" ? "0px" : "0.5em",
|
||||
],
|
||||
// opacity: {
|
||||
// duration: animTime / 2,
|
||||
// from: previous === "zen" ? 0 : 1,
|
||||
// to: current === "zen" ? 0 : 1,
|
||||
// },
|
||||
duration: animTime,
|
||||
ease: easing.both,
|
||||
onComplete: () => {
|
||||
if (current === "zen") {
|
||||
rightSpacerEl.classList.add("hidden");
|
||||
} else {
|
||||
rightSpacerEl.style.width = "";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const currentEl = $(`#testConfig .${submenu[current]}`);
|
||||
const previousEl = $(`#testConfig .${submenu[previous]}`);
|
||||
|
|
@ -171,7 +217,6 @@ async function update(previous: Mode, current: Mode): Promise<void> {
|
|||
);
|
||||
|
||||
previousEl.addClass("hidden");
|
||||
|
||||
currentEl.removeClass("hidden");
|
||||
|
||||
const currentWidth = Math.round(
|
||||
|
|
@ -179,53 +224,37 @@ async function update(previous: Mode, current: Mode): Promise<void> {
|
|||
);
|
||||
|
||||
previousEl.removeClass("hidden");
|
||||
|
||||
currentEl.addClass("hidden");
|
||||
|
||||
const widthDifference = currentWidth - previousWidth;
|
||||
|
||||
const widthStep = widthDifference / 2;
|
||||
|
||||
await promiseAnimate(previousEl[0] as HTMLElement, {
|
||||
opacity: [1, 0],
|
||||
width: [previousWidth + "px", previousWidth + widthStep + "px"],
|
||||
duration: animTime / 2,
|
||||
ease: easing.in,
|
||||
});
|
||||
|
||||
previousEl
|
||||
.stop(true, false)
|
||||
.css({
|
||||
opacity: 1,
|
||||
width: previousWidth,
|
||||
width: "unset",
|
||||
})
|
||||
.animate(
|
||||
{
|
||||
width: previousWidth + widthStep,
|
||||
opacity: 0,
|
||||
},
|
||||
animTime / 2,
|
||||
easing.in,
|
||||
() => {
|
||||
previousEl
|
||||
.css({
|
||||
opacity: 1,
|
||||
width: "unset",
|
||||
})
|
||||
.addClass("hidden");
|
||||
currentEl
|
||||
.css({
|
||||
opacity: 0,
|
||||
width: previousWidth + widthStep,
|
||||
})
|
||||
.removeClass("hidden")
|
||||
.stop(true, false)
|
||||
.animate(
|
||||
{
|
||||
opacity: 1,
|
||||
width: currentWidth,
|
||||
},
|
||||
animTime / 2,
|
||||
easing.out,
|
||||
() => {
|
||||
currentEl.css("width", "unset");
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
.addClass("hidden");
|
||||
currentEl
|
||||
.css({
|
||||
opacity: 0,
|
||||
width: previousWidth + widthStep + "px",
|
||||
})
|
||||
.removeClass("hidden");
|
||||
|
||||
await promiseAnimate(currentEl[0] as HTMLElement, {
|
||||
opacity: [0, 1],
|
||||
width: [previousWidth + widthStep + "px", currentWidth + "px"],
|
||||
duration: animTime / 2,
|
||||
ease: easing.out,
|
||||
});
|
||||
}
|
||||
|
||||
function updateActiveModeButtons(mode: Mode): void {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ import * as Sentry from "../sentry";
|
|||
import * as Loader from "../elements/loader";
|
||||
import * as TestInitFailed from "../elements/test-init-failed";
|
||||
import { canQuickRestart } from "../utils/quick-restart";
|
||||
import { animate } from "animejs";
|
||||
|
||||
let failReason = "";
|
||||
const koInputVisual = document.getElementById("koInputVisual") as HTMLElement;
|
||||
|
|
@ -307,22 +308,21 @@ export function restart(options = {} as RestartOptions): void {
|
|||
ConnectionState.showOfflineBanner();
|
||||
}
|
||||
|
||||
let el = null;
|
||||
let el: HTMLElement;
|
||||
if (TestState.resultVisible) {
|
||||
//results are being displayed
|
||||
el = $("#result");
|
||||
el = document.querySelector("#result") as HTMLElement;
|
||||
} else {
|
||||
//words are being displayed
|
||||
el = $("#typingTest");
|
||||
el = document.querySelector("#typingTest") as HTMLElement;
|
||||
}
|
||||
TestState.setResultVisible(false);
|
||||
TestState.setTestRestarting(true);
|
||||
el.stop(true, true).animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
animationTime,
|
||||
async () => {
|
||||
|
||||
animate(el, {
|
||||
opacity: 0,
|
||||
duration: animationTime,
|
||||
onComplete: async () => {
|
||||
$("#result").addClass("hidden");
|
||||
$("#typingTest").css("opacity", 0).removeClass("hidden");
|
||||
$("#wordsInput").css({ left: 0 }).val(" ");
|
||||
|
|
@ -382,27 +382,26 @@ export function restart(options = {} as RestartOptions): void {
|
|||
if (isWordsFocused) OutOfFocus.hide();
|
||||
TestUI.focusWords(true);
|
||||
|
||||
$("#typingTest")
|
||||
.css("opacity", 0)
|
||||
.removeClass("hidden")
|
||||
.stop(true, true)
|
||||
.animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
animationTime,
|
||||
() => {
|
||||
TimerProgress.reset();
|
||||
LiveSpeed.reset();
|
||||
LiveAcc.reset();
|
||||
LiveBurst.reset();
|
||||
TestUI.updatePremid();
|
||||
ManualRestart.reset();
|
||||
TestState.setTestRestarting(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
const typingTestEl = document.querySelector("#typingTest") as HTMLElement;
|
||||
|
||||
animate(typingTestEl, {
|
||||
opacity: [0, 1],
|
||||
onBegin: () => {
|
||||
typingTestEl.classList.remove("hidden");
|
||||
},
|
||||
duration: animationTime,
|
||||
onComplete: () => {
|
||||
TimerProgress.reset();
|
||||
LiveSpeed.reset();
|
||||
LiveAcc.reset();
|
||||
LiveBurst.reset();
|
||||
TestUI.updatePremid();
|
||||
ManualRestart.reset();
|
||||
TestState.setTestRestarting(false);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
ResultWordHighlight.destroy();
|
||||
}
|
||||
|
|
@ -1396,22 +1395,21 @@ async function saveResult(
|
|||
Result.showErrorCrownIfNeeded();
|
||||
}
|
||||
|
||||
const dailyLeaderboardEl = document.querySelector(
|
||||
"#result .stats .dailyLeaderboard"
|
||||
) as HTMLElement;
|
||||
|
||||
if (data.dailyLeaderboardRank === undefined) {
|
||||
$("#result .stats .dailyLeaderboard").addClass("hidden");
|
||||
dailyLeaderboardEl.classList.add("hidden");
|
||||
} else {
|
||||
$("#result .stats .dailyLeaderboard")
|
||||
.css({
|
||||
maxWidth: "13rem",
|
||||
opacity: 0,
|
||||
})
|
||||
.removeClass("hidden")
|
||||
.animate(
|
||||
{
|
||||
// maxWidth: "10rem",
|
||||
opacity: 1,
|
||||
},
|
||||
Misc.applyReducedMotion(500)
|
||||
);
|
||||
dailyLeaderboardEl.classList.remove("hidden");
|
||||
dailyLeaderboardEl.style.maxWidth = "13rem";
|
||||
|
||||
animate(dailyLeaderboardEl, {
|
||||
opacity: [0, 1],
|
||||
duration: Misc.applyReducedMotion(250),
|
||||
});
|
||||
|
||||
$("#result .stats .dailyLeaderboard .bottom").html(
|
||||
Format.rank(data.dailyLeaderboardRank, { fallback: "" })
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import * as TimerEvent from "../observables/timer-event";
|
|||
import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer";
|
||||
import { KeymapLayout, Layout } from "@monkeytype/schemas/configs";
|
||||
import * as SoundController from "../controllers/sound-controller";
|
||||
import { clearLowFpsMode, setLowFpsMode } from "../anim";
|
||||
|
||||
type TimerStats = {
|
||||
dateNow: number;
|
||||
|
|
@ -37,6 +38,7 @@ export function enableTimerDebug(): void {
|
|||
}
|
||||
|
||||
export function clear(): void {
|
||||
clearLowFpsMode();
|
||||
Time.set(0);
|
||||
if (timer !== null) clearTimeout(timer);
|
||||
}
|
||||
|
|
@ -239,6 +241,7 @@ export async function start(): Promise<void> {
|
|||
if (delay < interval / 2) {
|
||||
//slow timer
|
||||
SlowTimer.set();
|
||||
setLowFpsMode();
|
||||
}
|
||||
if (delay < interval / 10) {
|
||||
slowTimerCount++;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import * as Strings from "../utils/strings";
|
|||
import * as JSONData from "../utils/json-data";
|
||||
import { blendTwoHexColors } from "../utils/colors";
|
||||
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
|
||||
import * as SlowTimer from "../states/slow-timer";
|
||||
import * as CompositionState from "../states/composition";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
import * as Hangul from "hangul-js";
|
||||
|
|
@ -25,6 +24,7 @@ import { findSingleActiveFunboxWithFunction } from "./funbox/list";
|
|||
import * as TestState from "./test-state";
|
||||
import * as PaceCaret from "./pace-caret";
|
||||
import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame";
|
||||
import { animate } from "animejs";
|
||||
|
||||
const debouncedZipfCheck = debounce(250, async () => {
|
||||
const supports = await JSONData.checkIfLanguageSupportsZipf(Config.language);
|
||||
|
|
@ -1053,32 +1053,32 @@ export async function scrollTape(noAnimation = false): Promise<void> {
|
|||
newMargin = wordRightMargin - newMargin;
|
||||
}
|
||||
|
||||
const duration = noAnimation ? 0 : SlowTimer.get() ? 0 : 125;
|
||||
const duration = noAnimation ? 0 : 125;
|
||||
const ease = "inOut(1.25)";
|
||||
|
||||
const caretScrollOptions = {
|
||||
newValue: newMarginOffset * -1,
|
||||
duration: Config.smoothLineScroll ? duration : 0,
|
||||
ease,
|
||||
};
|
||||
|
||||
Caret.caret.handleTapeScroll(caretScrollOptions);
|
||||
PaceCaret.caret.handleTapeScroll(caretScrollOptions);
|
||||
|
||||
if (Config.smoothLineScroll) {
|
||||
const jqWords = $(wordsEl).stop("marginLeft", true, false);
|
||||
jqWords.animate(
|
||||
{
|
||||
marginLeft: newMargin,
|
||||
},
|
||||
{
|
||||
duration,
|
||||
queue: "marginLeft",
|
||||
}
|
||||
);
|
||||
jqWords.dequeue("marginLeft");
|
||||
animate(wordsEl, {
|
||||
marginLeft: newMargin,
|
||||
duration,
|
||||
ease,
|
||||
});
|
||||
|
||||
for (let i = 0; i < afterNewlinesNewMargins.length; i++) {
|
||||
const newMargin = afterNewlinesNewMargins[i] ?? 0;
|
||||
$(afterNewLineEls[i] as Element)
|
||||
.stop(true, false)
|
||||
.animate({ marginLeft: newMargin }, duration);
|
||||
animate(afterNewLineEls[i] as Element, {
|
||||
marginLeft: newMargin,
|
||||
duration,
|
||||
ease,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
wordsEl.style.marginLeft = `${newMargin}px`;
|
||||
|
|
@ -1166,7 +1166,7 @@ export async function lineJump(
|
|||
|
||||
const wordHeight = $(activeWordEl).outerHeight(true) as number;
|
||||
const newMarginTop = -1 * wordHeight * currentLinesJumping;
|
||||
const duration = SlowTimer.get() ? 0 : 125;
|
||||
const duration = 125;
|
||||
|
||||
const caretLineJumpOptions = {
|
||||
newMarginTop,
|
||||
|
|
@ -1177,23 +1177,18 @@ export async function lineJump(
|
|||
|
||||
if (Config.smoothLineScroll) {
|
||||
lineTransition = true;
|
||||
const jqWords = $(wordsEl);
|
||||
jqWords.stop("marginTop", true, false).animate(
|
||||
{ marginTop: `${newMarginTop}px` },
|
||||
{
|
||||
duration,
|
||||
queue: "marginTop",
|
||||
complete: () => {
|
||||
currentLinesJumping = 0;
|
||||
activeWordTop = activeWordEl.offsetTop;
|
||||
removeTestElements(lastElementIndexToRemove);
|
||||
wordsEl.style.marginTop = "0";
|
||||
lineTransition = false;
|
||||
resolve();
|
||||
},
|
||||
}
|
||||
);
|
||||
jqWords.dequeue("marginTop");
|
||||
animate(wordsEl, {
|
||||
marginTop: newMarginTop,
|
||||
duration,
|
||||
onComplete: () => {
|
||||
currentLinesJumping = 0;
|
||||
activeWordTop = activeWordEl.offsetTop;
|
||||
removeTestElements(lastElementIndexToRemove);
|
||||
wordsEl.style.marginTop = "0";
|
||||
lineTransition = false;
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
currentLinesJumping = 0;
|
||||
removeTestElements(lastElementIndexToRemove);
|
||||
|
|
|
|||
|
|
@ -4,42 +4,46 @@ import * as DateTime from "../utils/date-and-time";
|
|||
import * as TestWords from "./test-words";
|
||||
import * as TestInput from "./test-input";
|
||||
import * as Time from "../states/time";
|
||||
import * as SlowTimer from "../states/slow-timer";
|
||||
import * as TestState from "./test-state";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
import { applyReducedMotion } from "../utils/misc";
|
||||
import { animate } from "animejs";
|
||||
|
||||
const barEl = $("#barTimerProgress .bar");
|
||||
const barOpacityEl = $("#barTimerProgress .opacityWrapper");
|
||||
const textEl = $("#liveStatsTextTop .timerProgress");
|
||||
const miniEl = $("#liveStatsMini .time");
|
||||
const barEl = document.querySelector("#barTimerProgress .bar") as HTMLElement;
|
||||
const barOpacityEl = document.querySelector(
|
||||
"#barTimerProgress .opacityWrapper"
|
||||
) as HTMLElement;
|
||||
const textEl = document.querySelector(
|
||||
"#liveStatsTextTop .timerProgress"
|
||||
) as HTMLElement;
|
||||
const miniEl = document.querySelector("#liveStatsMini .time") as HTMLElement;
|
||||
|
||||
export function show(): void {
|
||||
if (!TestState.isActive) return;
|
||||
if (Config.mode !== "zen" && Config.timerStyle === "bar") {
|
||||
barOpacityEl
|
||||
.stop(true, true)
|
||||
.removeClass("hidden")
|
||||
.css("opacity", 0)
|
||||
.animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
animate(barOpacityEl, {
|
||||
opacity: [0, 1],
|
||||
duration: applyReducedMotion(125),
|
||||
onBegin: () => {
|
||||
barOpacityEl.classList.remove("hidden");
|
||||
},
|
||||
});
|
||||
} else if (Config.timerStyle === "text") {
|
||||
textEl.stop(true, true).removeClass("hidden").css("opacity", 0).animate(
|
||||
{
|
||||
opacity: 1,
|
||||
animate(textEl, {
|
||||
opacity: [0, 1],
|
||||
duration: applyReducedMotion(125),
|
||||
onBegin: () => {
|
||||
textEl.classList.remove("hidden");
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
});
|
||||
} else if (Config.mode === "zen" || Config.timerStyle === "mini") {
|
||||
miniEl.stop(true, true).removeClass("hidden").css("opacity", 0).animate(
|
||||
{
|
||||
opacity: 1,
|
||||
animate(miniEl, {
|
||||
opacity: [0, 1],
|
||||
duration: applyReducedMotion(125),
|
||||
onBegin: () => {
|
||||
miniEl.classList.remove("hidden");
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,42 +55,37 @@ export function reset(): void {
|
|||
) {
|
||||
width = "100vw";
|
||||
}
|
||||
barEl.stop(true, true).animate(
|
||||
{
|
||||
width,
|
||||
},
|
||||
0
|
||||
);
|
||||
miniEl.text("0");
|
||||
textEl.text("0");
|
||||
|
||||
animate(barEl, {
|
||||
width,
|
||||
duration: 0,
|
||||
});
|
||||
miniEl.textContent = "0";
|
||||
textEl.textContent = "0";
|
||||
}
|
||||
|
||||
export function hide(): void {
|
||||
barOpacityEl.stop(true, true).animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
miniEl.stop(true, true).animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
applyReducedMotion(125),
|
||||
() => {
|
||||
miniEl.addClass("hidden");
|
||||
}
|
||||
);
|
||||
textEl.stop(true, true).animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
applyReducedMotion(125)
|
||||
);
|
||||
}
|
||||
animate(barOpacityEl, {
|
||||
opacity: 0,
|
||||
duration: applyReducedMotion(125),
|
||||
});
|
||||
|
||||
const timerNumberElement = textEl[0] as HTMLElement;
|
||||
const miniTimerNumberElement = miniEl[0] as HTMLElement;
|
||||
animate(miniEl, {
|
||||
opacity: 0,
|
||||
duration: applyReducedMotion(125),
|
||||
onComplete: () => {
|
||||
miniEl.classList.add("hidden");
|
||||
},
|
||||
});
|
||||
|
||||
animate(textEl, {
|
||||
opacity: 0,
|
||||
duration: applyReducedMotion(125),
|
||||
onComplete: () => {
|
||||
textEl.classList.add("hidden");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentCount(): number {
|
||||
if (Config.mode === "custom" && CustomText.getLimitMode() === "section") {
|
||||
|
|
@ -111,28 +110,27 @@ export function update(): void {
|
|||
}
|
||||
if (Config.timerStyle === "bar") {
|
||||
const percent = 100 - ((time + 1) / maxtime) * 100;
|
||||
barEl.stop(true, true).animate(
|
||||
{
|
||||
width: percent + "vw",
|
||||
},
|
||||
SlowTimer.get() ? 0 : 1000,
|
||||
"linear"
|
||||
);
|
||||
|
||||
animate(barEl, {
|
||||
width: percent + "vw",
|
||||
duration: 1000,
|
||||
ease: "linear",
|
||||
});
|
||||
} else if (Config.timerStyle === "text") {
|
||||
let displayTime = DateTime.secondsToString(maxtime - time);
|
||||
if (maxtime === 0) {
|
||||
displayTime = DateTime.secondsToString(time);
|
||||
}
|
||||
if (timerNumberElement !== null) {
|
||||
timerNumberElement.innerHTML = "<div>" + displayTime + "</div>";
|
||||
if (textEl !== null) {
|
||||
textEl.innerHTML = "<div>" + displayTime + "</div>";
|
||||
}
|
||||
} else if (Config.timerStyle === "mini") {
|
||||
let displayTime = DateTime.secondsToString(maxtime - time);
|
||||
if (maxtime === 0) {
|
||||
displayTime = DateTime.secondsToString(time);
|
||||
}
|
||||
if (miniTimerNumberElement !== null) {
|
||||
miniTimerNumberElement.innerHTML = displayTime;
|
||||
if (miniEl !== null) {
|
||||
miniEl.innerHTML = displayTime;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
|
|
@ -154,50 +152,29 @@ export function update(): void {
|
|||
const percent = Math.floor(
|
||||
((TestState.activeWordIndex + 1) / outof) * 100
|
||||
);
|
||||
barEl.stop(true, true).animate(
|
||||
{
|
||||
width: percent + "vw",
|
||||
},
|
||||
SlowTimer.get() ? 0 : 250
|
||||
);
|
||||
|
||||
animate(barEl, {
|
||||
width: percent + "vw",
|
||||
duration: 250,
|
||||
});
|
||||
} else if (Config.timerStyle === "text") {
|
||||
if (outof === 0) {
|
||||
if (timerNumberElement !== null) {
|
||||
timerNumberElement.innerHTML = `<div>${
|
||||
TestInput.input.getHistory().length
|
||||
}</div>`;
|
||||
}
|
||||
textEl.innerHTML = `<div>${TestInput.input.getHistory().length}</div>`;
|
||||
} else {
|
||||
if (timerNumberElement !== null) {
|
||||
timerNumberElement.innerHTML = `<div>${getCurrentCount()}/${outof}</div>`;
|
||||
}
|
||||
textEl.innerHTML = `<div>${getCurrentCount()}/${outof}</div>`;
|
||||
}
|
||||
} else if (Config.timerStyle === "mini") {
|
||||
if (outof === 0) {
|
||||
if (miniTimerNumberElement !== null) {
|
||||
miniTimerNumberElement.innerHTML = `${
|
||||
TestInput.input.getHistory().length
|
||||
}`;
|
||||
}
|
||||
miniEl.innerHTML = `${TestInput.input.getHistory().length}`;
|
||||
} else {
|
||||
if (miniTimerNumberElement !== null) {
|
||||
miniTimerNumberElement.innerHTML = `${getCurrentCount()}/${outof}`;
|
||||
}
|
||||
miniEl.innerHTML = `${getCurrentCount()}/${outof}`;
|
||||
}
|
||||
}
|
||||
} else if (Config.mode === "zen") {
|
||||
if (Config.timerStyle === "text") {
|
||||
if (timerNumberElement !== null) {
|
||||
timerNumberElement.innerHTML = `<div>${
|
||||
TestInput.input.getHistory().length
|
||||
}</div>`;
|
||||
}
|
||||
textEl.innerHTML = `<div>${TestInput.input.getHistory().length}</div>`;
|
||||
} else {
|
||||
if (miniTimerNumberElement !== null) {
|
||||
miniTimerNumberElement.innerHTML = `${
|
||||
TestInput.input.getHistory().length
|
||||
}`;
|
||||
}
|
||||
miniEl.innerHTML = `${TestInput.input.getHistory().length}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import { animate, AnimationParams } from "animejs";
|
||||
import { applyReducedMotion, isPopupVisible } from "./misc";
|
||||
import * as Skeleton from "./skeleton";
|
||||
|
||||
type CustomAnimation = {
|
||||
from: Record<string, string>;
|
||||
to: Record<string, string>;
|
||||
easing?: string;
|
||||
durationMs?: number;
|
||||
};
|
||||
|
||||
type CustomWrapperAndModalAnimations = {
|
||||
wrapper?: CustomAnimation;
|
||||
modal?: CustomAnimation;
|
||||
wrapper?: AnimationParams & {
|
||||
duration?: number;
|
||||
};
|
||||
modal?: AnimationParams & {
|
||||
duration?: number;
|
||||
};
|
||||
};
|
||||
|
||||
type ConstructorCustomAnimations = {
|
||||
|
|
@ -217,9 +215,9 @@ export default class AnimatedModal<
|
|||
}
|
||||
|
||||
const modalAnimationDuration = applyReducedMotion(
|
||||
(options?.customAnimation?.modal?.durationMs ??
|
||||
(options?.customAnimation?.modal?.duration ??
|
||||
options?.animationDurationMs ??
|
||||
this.customShowAnimations?.modal?.durationMs ??
|
||||
this.customShowAnimations?.modal?.duration ??
|
||||
DEFAULT_ANIMATION_DURATION) *
|
||||
(options?.modalChain !== undefined
|
||||
? MODAL_ONLY_ANIMATION_MULTIPLIER
|
||||
|
|
@ -250,17 +248,18 @@ export default class AnimatedModal<
|
|||
this.focusFirstInput(options?.focusFirstInput);
|
||||
}, 1);
|
||||
|
||||
const modalAnimation =
|
||||
options?.customAnimation?.modal ?? this.customShowAnimations?.modal;
|
||||
const modalAnimation = options?.customAnimation?.modal ??
|
||||
this.customShowAnimations?.modal ?? {
|
||||
opacity: [0, 1],
|
||||
marginTop: ["1rem", 0],
|
||||
};
|
||||
const wrapperAnimation = options?.customAnimation?.wrapper ??
|
||||
this.customShowAnimations?.wrapper ?? {
|
||||
from: { opacity: "0" },
|
||||
to: { opacity: "1" },
|
||||
easing: "swing",
|
||||
opacity: [0, 1],
|
||||
};
|
||||
const wrapperAnimationDuration = applyReducedMotion(
|
||||
options?.customAnimation?.wrapper?.durationMs ??
|
||||
this.customShowAnimations?.wrapper?.durationMs ??
|
||||
options?.customAnimation?.wrapper?.duration ??
|
||||
this.customShowAnimations?.wrapper?.duration ??
|
||||
DEFAULT_ANIMATION_DURATION
|
||||
);
|
||||
|
||||
|
|
@ -269,59 +268,42 @@ export default class AnimatedModal<
|
|||
? "modalOnly"
|
||||
: options?.animationMode ?? "both";
|
||||
|
||||
$(this.modalEl).stop(true, false);
|
||||
$(this.wrapperEl).stop(true, false);
|
||||
|
||||
if (animationMode === "both" || animationMode === "none") {
|
||||
if (modalAnimation?.from) {
|
||||
$(this.modalEl).css(modalAnimation.from);
|
||||
$(this.modalEl).animate(
|
||||
modalAnimation.to,
|
||||
animationMode === "none" ? 0 : modalAnimationDuration,
|
||||
modalAnimation.easing ?? "swing"
|
||||
);
|
||||
} else {
|
||||
$(this.modalEl).css("opacity", "1");
|
||||
}
|
||||
animate(this.modalEl, {
|
||||
...modalAnimation,
|
||||
duration: animationMode === "none" ? 0 : modalAnimationDuration,
|
||||
});
|
||||
|
||||
$(this.wrapperEl).css(wrapperAnimation.from);
|
||||
$(this.wrapperEl)
|
||||
.removeClass("hidden")
|
||||
.css("opacity", "0")
|
||||
.animate(
|
||||
wrapperAnimation.to ?? { opacity: 1 },
|
||||
animationMode === "none" ? 0 : wrapperAnimationDuration,
|
||||
wrapperAnimation.easing ?? "swing",
|
||||
async () => {
|
||||
this.focusFirstInput(options?.focusFirstInput);
|
||||
await options?.afterAnimation?.(
|
||||
this.modalEl,
|
||||
options?.modalChainData
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
} else if (animationMode === "modalOnly") {
|
||||
$(this.wrapperEl).removeClass("hidden").css("opacity", "1");
|
||||
|
||||
if (modalAnimation?.from) {
|
||||
$(this.modalEl).css(modalAnimation.from);
|
||||
} else {
|
||||
$(this.modalEl).css("opacity", "0");
|
||||
}
|
||||
$(this.modalEl).animate(
|
||||
modalAnimation?.to ?? { opacity: 1 },
|
||||
modalAnimationDuration,
|
||||
modalAnimation?.easing ?? "swing",
|
||||
async () => {
|
||||
animate(this.wrapperEl, {
|
||||
...wrapperAnimation,
|
||||
duration: animationMode === "none" ? 0 : wrapperAnimationDuration,
|
||||
onBegin: () => {
|
||||
this.wrapperEl.classList.remove("hidden");
|
||||
},
|
||||
onComplete: async () => {
|
||||
this.focusFirstInput(options?.focusFirstInput);
|
||||
await options?.afterAnimation?.(
|
||||
this.modalEl,
|
||||
options?.modalChainData
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
} else if (animationMode === "modalOnly") {
|
||||
$(this.wrapperEl).removeClass("hidden").css("opacity", "1");
|
||||
|
||||
animate(this.modalEl, {
|
||||
...modalAnimation,
|
||||
duration: modalAnimationDuration,
|
||||
onComplete: async () => {
|
||||
this.focusFirstInput(options?.focusFirstInput);
|
||||
await options?.afterAnimation?.(
|
||||
this.modalEl,
|
||||
options?.modalChainData
|
||||
);
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -340,12 +322,15 @@ export default class AnimatedModal<
|
|||
|
||||
await options?.beforeAnimation?.(this.modalEl);
|
||||
|
||||
const modalAnimation =
|
||||
options?.customAnimation?.modal ?? this.customHideAnimations?.modal;
|
||||
const modalAnimation = options?.customAnimation?.modal ??
|
||||
this.customHideAnimations?.modal ?? {
|
||||
opacity: [1, 0],
|
||||
marginTop: [0, "1rem"],
|
||||
};
|
||||
const modalAnimationDuration = applyReducedMotion(
|
||||
(options?.customAnimation?.modal?.durationMs ??
|
||||
(options?.customAnimation?.modal?.duration ??
|
||||
options?.animationDurationMs ??
|
||||
this.customHideAnimations?.modal?.durationMs ??
|
||||
this.customHideAnimations?.modal?.duration ??
|
||||
DEFAULT_ANIMATION_DURATION) *
|
||||
(this.previousModalInChain !== undefined
|
||||
? MODAL_ONLY_ANIMATION_MULTIPLIER
|
||||
|
|
@ -353,13 +338,11 @@ export default class AnimatedModal<
|
|||
);
|
||||
const wrapperAnimation = options?.customAnimation?.wrapper ??
|
||||
this.customHideAnimations?.wrapper ?? {
|
||||
from: { opacity: "1" },
|
||||
to: { opacity: "0" },
|
||||
easing: "swing",
|
||||
opacity: [1, 0],
|
||||
};
|
||||
const wrapperAnimationDuration = applyReducedMotion(
|
||||
options?.customAnimation?.wrapper?.durationMs ??
|
||||
this.customHideAnimations?.wrapper?.durationMs ??
|
||||
options?.customAnimation?.wrapper?.duration ??
|
||||
this.customHideAnimations?.wrapper?.duration ??
|
||||
DEFAULT_ANIMATION_DURATION
|
||||
);
|
||||
const animationMode =
|
||||
|
|
@ -367,66 +350,47 @@ export default class AnimatedModal<
|
|||
? "modalOnly"
|
||||
: options?.animationMode ?? "both";
|
||||
|
||||
$(this.modalEl).stop(true, false);
|
||||
$(this.wrapperEl).stop(true, false);
|
||||
|
||||
if (animationMode === "both" || animationMode === "none") {
|
||||
if (modalAnimation?.from) {
|
||||
$(this.modalEl).css(modalAnimation.from);
|
||||
$(this.modalEl).animate(
|
||||
modalAnimation.to,
|
||||
animationMode === "none" ? 0 : modalAnimationDuration,
|
||||
modalAnimation.easing ?? "swing"
|
||||
);
|
||||
} else {
|
||||
$(this.modalEl).css("opacity", "1");
|
||||
}
|
||||
animate(this.modalEl, {
|
||||
...modalAnimation,
|
||||
duration: animationMode === "none" ? 0 : modalAnimationDuration,
|
||||
});
|
||||
|
||||
$(this.wrapperEl).css(wrapperAnimation.from);
|
||||
$(this.wrapperEl)
|
||||
.css("opacity", "1")
|
||||
.animate(
|
||||
wrapperAnimation?.to ?? { opacity: 0 },
|
||||
animationMode === "none" ? 0 : wrapperAnimationDuration,
|
||||
wrapperAnimation?.easing ?? "swing",
|
||||
async () => {
|
||||
this.wrapperEl.close();
|
||||
this.wrapperEl.classList.add("hidden");
|
||||
Skeleton.remove(this.dialogId);
|
||||
this.open = false;
|
||||
await options?.afterAnimation?.(this.modalEl);
|
||||
void this.cleanup?.();
|
||||
animate(this.wrapperEl, {
|
||||
...wrapperAnimation,
|
||||
duration: animationMode === "none" ? 0 : wrapperAnimationDuration,
|
||||
onComplete: async () => {
|
||||
this.wrapperEl.close();
|
||||
this.wrapperEl.classList.add("hidden");
|
||||
Skeleton.remove(this.dialogId);
|
||||
this.open = false;
|
||||
await options?.afterAnimation?.(this.modalEl);
|
||||
void this.cleanup?.();
|
||||
|
||||
if (
|
||||
this.previousModalInChain !== undefined &&
|
||||
!options?.dontShowPreviousModalInchain
|
||||
) {
|
||||
await this.previousModalInChain.show({
|
||||
animationMode: "modalOnly",
|
||||
modalChainData: options?.modalChainData,
|
||||
animationDurationMs:
|
||||
modalAnimationDuration * MODAL_ONLY_ANIMATION_MULTIPLIER,
|
||||
...this.previousModalInChain.showOptionsWhenInChain,
|
||||
});
|
||||
this.previousModalInChain = undefined;
|
||||
}
|
||||
|
||||
resolve();
|
||||
if (
|
||||
this.previousModalInChain !== undefined &&
|
||||
!options?.dontShowPreviousModalInchain
|
||||
) {
|
||||
await this.previousModalInChain.show({
|
||||
animationMode: "modalOnly",
|
||||
modalChainData: options?.modalChainData,
|
||||
animationDurationMs:
|
||||
modalAnimationDuration * MODAL_ONLY_ANIMATION_MULTIPLIER,
|
||||
...this.previousModalInChain.showOptionsWhenInChain,
|
||||
});
|
||||
this.previousModalInChain = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
} else if (animationMode === "modalOnly") {
|
||||
$(this.wrapperEl).removeClass("hidden").css("opacity", "1");
|
||||
|
||||
if (modalAnimation?.from) {
|
||||
$(this.modalEl).css(modalAnimation.from);
|
||||
} else {
|
||||
$(this.modalEl).css("opacity", "1");
|
||||
}
|
||||
$(this.modalEl).animate(
|
||||
modalAnimation?.to ?? { opacity: 0 },
|
||||
modalAnimationDuration,
|
||||
modalAnimation?.easing ?? "swing",
|
||||
async () => {
|
||||
animate(this.modalEl, {
|
||||
...modalAnimation,
|
||||
duration: modalAnimationDuration,
|
||||
onComplete: async () => {
|
||||
this.wrapperEl.close();
|
||||
$(this.wrapperEl).addClass("hidden").css("opacity", "0");
|
||||
Skeleton.remove(this.dialogId);
|
||||
|
|
@ -449,8 +413,8 @@ export default class AnimatedModal<
|
|||
}
|
||||
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { CaretStyle } from "@monkeytype/schemas/configs";
|
||||
import Config from "../config";
|
||||
import * as SlowTimer from "../states/slow-timer";
|
||||
import * as TestWords from "../test/test-words";
|
||||
import { getTotalInlineMargin } from "./misc";
|
||||
import { isWordRightToLeft } from "./strings";
|
||||
import { requestDebouncedAnimationFrame } from "./debounced-animation-frame";
|
||||
import { animate, EasingParam, JSAnimation } from "animejs";
|
||||
|
||||
const wordsCache = document.querySelector<HTMLElement>("#words") as HTMLElement;
|
||||
const wordsWrapperCache = document.querySelector<HTMLElement>(
|
||||
|
|
@ -38,6 +38,10 @@ export class Caret {
|
|||
private isMainCaret: boolean = false;
|
||||
private cumulativeTapeMarginCorrection: number = 0;
|
||||
|
||||
private posAnimation: JSAnimation | null = null;
|
||||
private marginTopAnimation: JSAnimation | null = null;
|
||||
private marginLeftAnimation: JSAnimation | null = null;
|
||||
|
||||
constructor(element: HTMLElement, style: CaretStyle) {
|
||||
this.id = element.id;
|
||||
this.element = element;
|
||||
|
|
@ -102,7 +106,7 @@ export class Caret {
|
|||
top: number;
|
||||
width?: number;
|
||||
}): void {
|
||||
$(this.element).stop("pos", true, false);
|
||||
this.posAnimation?.cancel();
|
||||
this.element.style.left = `${options.left}px`;
|
||||
this.element.style.top = `${options.top}px`;
|
||||
if (options.width !== undefined) {
|
||||
|
|
@ -111,7 +115,7 @@ export class Caret {
|
|||
}
|
||||
|
||||
public startBlinking(): void {
|
||||
if (Config.smoothCaret !== "off" && !SlowTimer.get()) {
|
||||
if (Config.smoothCaret !== "off") {
|
||||
this.element.style.animationName = "caretFlashSmooth";
|
||||
} else {
|
||||
this.element.style.animationName = "caretFlashHard";
|
||||
|
|
@ -132,7 +136,9 @@ export class Caret {
|
|||
}
|
||||
|
||||
public stopAllAnimations(): void {
|
||||
$(this.element).stop(true, false);
|
||||
this.posAnimation?.cancel();
|
||||
this.marginTopAnimation?.cancel();
|
||||
this.marginLeftAnimation?.cancel();
|
||||
}
|
||||
|
||||
public clearMargins(): void {
|
||||
|
|
@ -152,6 +158,7 @@ export class Caret {
|
|||
public handleTapeScroll(options: {
|
||||
newValue: number;
|
||||
duration: number;
|
||||
ease: EasingParam;
|
||||
}): void {
|
||||
if (this.isMainCaret && lockedMainCaretInTape) return;
|
||||
this.readyToResetMarginLeft = false;
|
||||
|
|
@ -170,30 +177,20 @@ export class Caret {
|
|||
options.newValue - this.cumulativeTapeMarginCorrection;
|
||||
|
||||
if (options.duration === 0) {
|
||||
$(this.element).stop("marginLeft", true, false).css({
|
||||
marginLeft: newMarginLeft,
|
||||
});
|
||||
this.marginLeftAnimation?.cancel();
|
||||
this.element.style.marginLeft = `${newMarginLeft}px`;
|
||||
this.readyToResetMarginLeft = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$(this.element)
|
||||
.stop("marginLeft", true, false)
|
||||
.animate(
|
||||
{
|
||||
marginLeft: newMarginLeft,
|
||||
},
|
||||
{
|
||||
// this NEEDS to be the same duration as the
|
||||
// line scroll otherwise it will look weird
|
||||
duration: options.duration,
|
||||
queue: "marginLeft",
|
||||
complete: () => {
|
||||
this.readyToResetMarginLeft = true;
|
||||
},
|
||||
}
|
||||
);
|
||||
$(this.element).dequeue("marginLeft");
|
||||
this.marginLeftAnimation = animate(this.element, {
|
||||
marginLeft: newMarginLeft,
|
||||
duration: options.duration,
|
||||
ease: options.ease,
|
||||
onComplete: () => {
|
||||
this.readyToResetMarginLeft = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public handleLineJump(options: {
|
||||
|
|
@ -212,37 +209,26 @@ export class Caret {
|
|||
this.readyToResetMarginTop = false;
|
||||
|
||||
if (options.duration === 0) {
|
||||
$(this.element).stop("marginTop", true, false).css({
|
||||
marginTop: options.newMarginTop,
|
||||
});
|
||||
this.marginTopAnimation?.cancel();
|
||||
this.element.style.marginTop = `${options.newMarginTop}px`;
|
||||
this.readyToResetMarginTop = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$(this.element)
|
||||
.stop("marginTop", true, false)
|
||||
.animate(
|
||||
{
|
||||
marginTop: options.newMarginTop,
|
||||
},
|
||||
{
|
||||
// this NEEDS to be the same duration as the
|
||||
// line scroll otherwise it will look weird
|
||||
duration: options.duration,
|
||||
queue: "marginTop",
|
||||
complete: () => {
|
||||
this.readyToResetMarginTop = true;
|
||||
},
|
||||
}
|
||||
);
|
||||
$(this.element).dequeue("marginTop");
|
||||
this.marginTopAnimation = animate(this.element, {
|
||||
marginTop: options.newMarginTop,
|
||||
duration: options.duration,
|
||||
onComplete: () => {
|
||||
this.readyToResetMarginTop = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public animatePosition(options: {
|
||||
left: number;
|
||||
top: number;
|
||||
duration?: number;
|
||||
easing?: string;
|
||||
easing?: EasingParam;
|
||||
width?: number;
|
||||
}): void {
|
||||
const smoothCaretSpeed =
|
||||
|
|
@ -256,9 +242,7 @@ export class Caret {
|
|||
? 85
|
||||
: 0;
|
||||
|
||||
const finalDuration = SlowTimer.get()
|
||||
? 0
|
||||
: options.duration ?? smoothCaretSpeed;
|
||||
const finalDuration = options.duration ?? smoothCaretSpeed;
|
||||
|
||||
const animation: Record<string, number> = {
|
||||
left: options.left,
|
||||
|
|
@ -269,14 +253,11 @@ export class Caret {
|
|||
animation["width"] = options.width;
|
||||
}
|
||||
|
||||
$(this.element)
|
||||
.stop("pos", true, false)
|
||||
.animate(animation, {
|
||||
duration: finalDuration,
|
||||
easing: options.easing ?? "swing",
|
||||
queue: "pos",
|
||||
});
|
||||
$(this.element).dequeue("pos");
|
||||
this.posAnimation = animate(this.element, {
|
||||
...animation,
|
||||
duration: finalDuration,
|
||||
ease: options.easing ?? "inOut(1.25)",
|
||||
});
|
||||
}
|
||||
|
||||
public goTo(options: {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Mode, Mode2, PersonalBests } from "@monkeytype/schemas/shared";
|
|||
import { Result } from "@monkeytype/schemas/results";
|
||||
import { RankAndCount } from "@monkeytype/schemas/users";
|
||||
import { roundTo2 } from "@monkeytype/util/numbers";
|
||||
import { animate, AnimationParams } from "animejs";
|
||||
|
||||
export function whorf(speed: number, wordlen: number): number {
|
||||
return Math.min(
|
||||
|
|
@ -226,8 +227,8 @@ type LastIndex = {
|
|||
export const trailingComposeChars = /[\u02B0-\u02FF`´^¨~]+$|⎄.*$/;
|
||||
|
||||
export async function swapElements(
|
||||
el1: JQuery,
|
||||
el2: JQuery,
|
||||
el1: HTMLElement,
|
||||
el2: HTMLElement,
|
||||
totalDuration: number,
|
||||
callback = async function (): Promise<void> {
|
||||
return Promise.resolve();
|
||||
|
|
@ -236,57 +237,49 @@ export async function swapElements(
|
|||
return Promise.resolve();
|
||||
}
|
||||
): Promise<boolean | undefined> {
|
||||
if (el1 === null || el2 === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
totalDuration = applyReducedMotion(totalDuration);
|
||||
if (
|
||||
(el1.hasClass("hidden") && !el2.hasClass("hidden")) ||
|
||||
(!el1.hasClass("hidden") && el2.hasClass("hidden"))
|
||||
(el1.classList.contains("hidden") && !el2.classList.contains("hidden")) ||
|
||||
(!el1.classList.contains("hidden") && el2.classList.contains("hidden"))
|
||||
) {
|
||||
//one of them is hidden and the other is visible
|
||||
if (el1.hasClass("hidden")) {
|
||||
if (el1.classList.contains("hidden")) {
|
||||
await middleCallback();
|
||||
await callback();
|
||||
return false;
|
||||
}
|
||||
$(el1)
|
||||
.removeClass("hidden")
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
totalDuration / 2,
|
||||
async () => {
|
||||
await middleCallback();
|
||||
$(el1).addClass("hidden");
|
||||
$(el2)
|
||||
.removeClass("hidden")
|
||||
.css("opacity", 0)
|
||||
.animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
totalDuration / 2,
|
||||
async () => {
|
||||
await callback();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
} else if (el1.hasClass("hidden") && el2.hasClass("hidden")) {
|
||||
|
||||
el1.classList.remove("hidden");
|
||||
await promiseAnimate(el1, {
|
||||
opacity: [1, 0],
|
||||
duration: totalDuration / 2,
|
||||
});
|
||||
el1.classList.add("hidden");
|
||||
await middleCallback();
|
||||
el2.classList.remove("hidden");
|
||||
await promiseAnimate(el2, {
|
||||
opacity: [0, 1],
|
||||
duration: totalDuration / 2,
|
||||
});
|
||||
await callback();
|
||||
} else if (
|
||||
el1.classList.contains("hidden") &&
|
||||
el2.classList.contains("hidden")
|
||||
) {
|
||||
//both are hidden, only fade in the second
|
||||
await middleCallback();
|
||||
$(el2)
|
||||
.removeClass("hidden")
|
||||
.css("opacity", 0)
|
||||
.animate(
|
||||
{
|
||||
opacity: 1,
|
||||
},
|
||||
totalDuration,
|
||||
async () => {
|
||||
await callback();
|
||||
}
|
||||
);
|
||||
|
||||
el2.classList.remove("hidden");
|
||||
await promiseAnimate(el2, {
|
||||
opacity: [0, 1],
|
||||
duration: totalDuration / 2,
|
||||
});
|
||||
|
||||
await callback();
|
||||
} else {
|
||||
await middleCallback();
|
||||
await callback();
|
||||
|
|
@ -489,14 +482,17 @@ export type JQueryEasing =
|
|||
| "easeOutBounce"
|
||||
| "easeInOutBounce";
|
||||
|
||||
export async function promiseAnimation(
|
||||
el: JQuery,
|
||||
animation: Record<string, string>,
|
||||
duration: number,
|
||||
easing: JQueryEasing = "swing"
|
||||
export async function promiseAnimate(
|
||||
el: HTMLElement,
|
||||
options: AnimationParams
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
el.animate(animation, applyReducedMotion(duration), easing, resolve);
|
||||
animate(el, {
|
||||
...options,
|
||||
onComplete: () => {
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
|
|
@ -285,6 +285,9 @@ importers:
|
|||
'@ts-rest/core':
|
||||
specifier: 3.52.1
|
||||
version: 3.52.1(@types/node@24.9.1)(zod@3.23.8)
|
||||
animejs:
|
||||
specifier: 4.2.2
|
||||
version: 4.2.2
|
||||
balloon-css:
|
||||
specifier: 1.2.0
|
||||
version: 1.2.0
|
||||
|
|
@ -330,12 +333,6 @@ importers:
|
|||
jquery:
|
||||
specifier: 3.7.1
|
||||
version: 3.7.1
|
||||
jquery-color:
|
||||
specifier: 2.2.0
|
||||
version: 2.2.0(jquery@3.7.1)
|
||||
jquery.easing:
|
||||
specifier: 1.4.1
|
||||
version: 1.4.1
|
||||
konami:
|
||||
specifier: 1.7.0
|
||||
version: 1.7.0
|
||||
|
|
@ -3545,6 +3542,9 @@ packages:
|
|||
ajv@8.17.1:
|
||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||
|
||||
animejs@4.2.2:
|
||||
resolution: {integrity: sha512-Ys3RuvLdAeI14fsdKCQy7ytu4057QX6Bb7m4jwmfd6iKmUmLquTwk1ut0e4NtRQgCeq/s2Lv5+oMBjz6c7ZuIg==}
|
||||
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
||||
|
|
@ -6329,14 +6329,6 @@ packages:
|
|||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
jquery-color@2.2.0:
|
||||
resolution: {integrity: sha512-4VoxsLMw860EQGNT/TmP3Lbr7/1OCQlBPS4ILj7bxRApJrPQfpqzdIOTY8Ll9nGY7UHtWqDuzR7cUcS1lcWjVw==}
|
||||
peerDependencies:
|
||||
jquery: '>=1.11.0'
|
||||
|
||||
jquery.easing@1.4.1:
|
||||
resolution: {integrity: sha512-BVpRacWCbNfo/ALWxnLkIY/WRa4Ydg/LtwzIJZvDm7vrhV8Txv+ACi6EGnU11zT19sTc3KEPathWx6CtjWLD1w==}
|
||||
|
||||
jquery@3.7.1:
|
||||
resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==}
|
||||
|
||||
|
|
@ -13139,6 +13131,8 @@ snapshots:
|
|||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
animejs@4.2.2: {}
|
||||
|
||||
ansi-align@3.0.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
|
@ -16590,12 +16584,6 @@ snapshots:
|
|||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
jquery-color@2.2.0(jquery@3.7.1):
|
||||
dependencies:
|
||||
jquery: 3.7.1
|
||||
|
||||
jquery.easing@1.4.1: {}
|
||||
|
||||
jquery@3.7.1: {}
|
||||
|
||||
js-beautify@1.15.1:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue