diff --git a/frontend/__tests__/test/funbox/funbox-validation.spec.ts b/frontend/__tests__/test/funbox/funbox-validation.spec.ts index 00c1e0825028..d8c94a6c1f98 100644 --- a/frontend/__tests__/test/funbox/funbox-validation.spec.ts +++ b/frontend/__tests__/test/funbox/funbox-validation.spec.ts @@ -23,7 +23,8 @@ describe("funbox-validation", () => { "rAnDoMcAsE", //changesCapitalisation "nospace", //nospace "plus_one", //toPush: - "read_ahead_easy", //changesWordVisibility + "read_ahead_easy", //changesWordsVisibility + "tunnel_vision", //changesWordsVisibility "tts", //speaks "layout_mirror", //changesLayout "zipf", //changesWordsFrequency diff --git a/frontend/src/ts/elements/caret.ts b/frontend/src/ts/elements/caret.ts index 15c326971653..49abb0fa59cf 100644 --- a/frontend/src/ts/elements/caret.ts +++ b/frontend/src/ts/elements/caret.ts @@ -282,7 +282,7 @@ export class Caret { easing?: string; }; }): void { - if (this.style === "off") return; + if (this.style === "off" && !this.isMainCaret) return; requestDebouncedAnimationFrame(`caret.${this.id}.goTo`, () => { const word = wordsCache.qs( `.word[data-wordindex="${options.wordIndex}"]`, diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index fb0a37ad2699..e80b2bbf276d 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -72,6 +72,31 @@ async function readAheadHandleKeydown(event: KeyboardEvent): Promise { } } +let tunnelVisionObserver: MutationObserver | undefined; +let tunnelVisionFrame: number | undefined; + +function requestCaretPositionUpdate(): void { + if (tunnelVisionFrame !== undefined) return; + tunnelVisionFrame = requestAnimationFrame(() => { + tunnelVisionFrame = undefined; + + const caret = document.getElementById("caret"); + const words = document.getElementById("words"); + if (caret === null || words === null) return; + + const caretRect = caret.getBoundingClientRect(); + const wordsRect = words.getBoundingClientRect(); + words.style.setProperty( + "--caret-center-x", + `${caretRect.left + caretRect.width / 2 - wordsRect.left}px`, + ); + words.style.setProperty( + "--caret-center-y", + `${caretRect.top + caretRect.height / 2 - wordsRect.top}px`, + ); + }); +} + //todo move to its own file class CharDistribution { public chars: Record; @@ -502,6 +527,31 @@ const list: Partial> = { await readAheadHandleKeydown(event); }, }, + tunnel_vision: { + applyConfig(): void { + if (tunnelVisionObserver !== undefined) return; + + const caret = document.getElementById("caret"); + if (caret === null) return; + + tunnelVisionObserver = new MutationObserver(requestCaretPositionUpdate); + tunnelVisionObserver.observe(caret, { + attributes: true, + attributeFilter: ["class", "style"], + }); + window.addEventListener("resize", requestCaretPositionUpdate); + }, + clearGlobal(): void { + tunnelVisionObserver?.disconnect(); + tunnelVisionObserver = undefined; + window.removeEventListener("resize", requestCaretPositionUpdate); + + if (tunnelVisionFrame !== undefined) { + cancelAnimationFrame(tunnelVisionFrame); + tunnelVisionFrame = undefined; + } + }, + }, memory: { applyConfig(): void { qs("#wordsWrapper")?.hide(); diff --git a/frontend/static/funbox/tunnel_vision.css b/frontend/static/funbox/tunnel_vision.css new file mode 100644 index 000000000000..6252fa4cda0e --- /dev/null +++ b/frontend/static/funbox/tunnel_vision.css @@ -0,0 +1,13 @@ +#words { + /* em keeps the visible area proportional to the current typing font size. */ + --tunnel-vision-radius: 3em; + --tunnel-vision-softness: 0.8em; + + mask-image: radial-gradient( + circle at var(--caret-center-x, 50%) var(--caret-center-y, 50%), + #000 0, + #000 var(--tunnel-vision-radius), + transparent + calc(var(--tunnel-vision-radius) + var(--tunnel-vision-softness)) + ); +} diff --git a/packages/funbox/src/list.ts b/packages/funbox/src/list.ts index 649ef7e2422a..aa875e6a3f58 100644 --- a/packages/funbox/src/list.ts +++ b/packages/funbox/src/list.ts @@ -268,6 +268,15 @@ const list: Record = { frontendFunctions: ["rememberSettings", "handleKeydown"], name: "read_ahead_hard", }, + tunnel_vision: { + description: "Only the area around the caret is visible.", + canGetPb: true, + difficultyLevel: 2, + properties: ["changesWordsVisibility", "hasCssFile"], + frontendFunctions: ["applyConfig", "clearGlobal"], + name: "tunnel_vision", + cssModifications: ["words"], + }, memory: { description: "Test your memory. Remember the words and type them blind.", canGetPb: true, diff --git a/packages/schemas/__tests__/config.spec.ts b/packages/schemas/__tests__/config.spec.ts index 50db32000f94..169982e4e276 100644 --- a/packages/schemas/__tests__/config.spec.ts +++ b/packages/schemas/__tests__/config.spec.ts @@ -1,5 +1,8 @@ import { describe, it, expect } from "vitest"; -import { CustomBackgroundSchema } from "@monkeytype/schemas/configs"; +import { + CustomBackgroundSchema, + FunboxNameSchema, +} from "@monkeytype/schemas/configs"; describe("config schema", () => { describe("CustomBackgroundSchema", () => { @@ -80,4 +83,10 @@ describe("config schema", () => { } }); }); + + describe("FunboxNameSchema", () => { + it("accepts tunnel vision", () => { + expect(FunboxNameSchema.safeParse("tunnel_vision").success).toBe(true); + }); + }); }); diff --git a/packages/schemas/src/configs.ts b/packages/schemas/src/configs.ts index c2cd9ee56b7f..2d2241fea999 100644 --- a/packages/schemas/src/configs.ts +++ b/packages/schemas/src/configs.ts @@ -309,6 +309,7 @@ export const FunboxNameSchema = z.enum([ "read_ahead_easy", "read_ahead", "read_ahead_hard", + "tunnel_vision", "memory", "nospace", "poetry",