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:
Jack 2025-11-17 12:59:56 +01:00 committed by GitHub
parent 1009791915
commit 2536087276
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1043 additions and 1135 deletions

View file

@ -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",

View file

@ -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>

View file

@ -193,7 +193,7 @@
}
.active {
animation: accountRowHighlight 4s linear 0s 1;
animation: accountRowHighlight 5s linear 0s 1;
}
.loadMoreButton {

View file

@ -69,7 +69,6 @@ nav {
}
.level {
transition: 0.125s;
width: max-content;
font-size: 0.65em;
line-height: 0.65em;

View file

@ -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;

View file

@ -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"

View file

@ -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
View 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();
}

View file

@ -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;
}

View file

@ -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();

View file

@ -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("");

View file

@ -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"],
},
},
},

View file

@ -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) ??

View file

@ -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) {}
}

View file

@ -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();
}
}
}

View 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());

View file

@ -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
);
}

View file

@ -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)",
});
}

View file

@ -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);

View file

@ -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 () => {
//

View file

@ -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) => {

View file

@ -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;
}
}

View file

@ -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,
});
}

View file

@ -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,

View file

@ -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);
},
});
}
}

View file

@ -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) {

View file

@ -17,8 +17,8 @@ export function hide(): void {
}
export function resetPosition(): void {
caret.clearMargins();
caret.stopAllAnimations();
caret.clearMargins();
caret.goTo({
wordIndex: 0,
letterIndex: 0,

View file

@ -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;
}

View file

@ -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`;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);
},
});
}

View file

@ -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 {

View file

@ -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,

View file

@ -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 {

View file

@ -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: "" })
);

View file

@ -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++;

View file

@ -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);

View file

@ -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}`;
}
}
}

View file

@ -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();
}
);
},
});
}
});
}

View file

@ -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: {

View file

@ -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
View file

@ -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: