From 3c0941d8cd4ad611f0394a2a534f242a6ba421f3 Mon Sep 17 00:00:00 2001 From: d1rshan Date: Fri, 15 May 2026 21:50:33 +0530 Subject: [PATCH 1/9] add tunnel vision funbox --- .../test/funbox/funbox-validation.spec.ts | 1 + frontend/src/ts/elements/caret.ts | 15 ++++++++++++++- frontend/static/funbox/tunnel_vision.css | 14 ++++++++++++++ packages/funbox/src/list.ts | 8 ++++++++ packages/schemas/__tests__/config.spec.ts | 11 ++++++++++- packages/schemas/src/configs.ts | 1 + 6 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 frontend/static/funbox/tunnel_vision.css diff --git a/frontend/__tests__/test/funbox/funbox-validation.spec.ts b/frontend/__tests__/test/funbox/funbox-validation.spec.ts index 00c1e0825028..e3aa0967d0c5 100644 --- a/frontend/__tests__/test/funbox/funbox-validation.spec.ts +++ b/frontend/__tests__/test/funbox/funbox-validation.spec.ts @@ -24,6 +24,7 @@ describe("funbox-validation", () => { "nospace", //nospace "plus_one", //toPush: "read_ahead_easy", //changesWordVisibility + "tunnel_vision", //changesWordVisibility "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..2010df10e203 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}"]`, @@ -378,6 +378,8 @@ export class Caret { ...(options.animate && options.animationOptions), }; + this.updateWordsCaretPosition(left, top); + if (options.animate) { this.animatePosition(animateOrPositionOptions); } else { @@ -552,4 +554,15 @@ export class Caret { width, }; } + + private updateWordsCaretPosition(left: number, top: number): void { + if (!this.isMainCaret) return; + + const style = wordsCache.native.style; + const centerX = left - wordsCache.getOffsetLeft() + this.getWidth() / 2; + const centerY = top - wordsCache.getOffsetTop() + this.getHeight() / 2; + + style.setProperty("--caret-center-x", `${centerX}px`); + style.setProperty("--caret-center-y", `${centerY}px`); + } } diff --git a/frontend/static/funbox/tunnel_vision.css b/frontend/static/funbox/tunnel_vision.css new file mode 100644 index 000000000000..4f7986d46a66 --- /dev/null +++ b/frontend/static/funbox/tunnel_vision.css @@ -0,0 +1,14 @@ +body.fb-tunnel-vision #words { + --tunnel-vision-radius: 4.5em; + --tunnel-vision-softness: 0.75em; + + 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)) + ); + mask-repeat: no-repeat; + mask-size: 100% 100%; +} diff --git a/packages/funbox/src/list.ts b/packages/funbox/src/list.ts index 19cd30abca89..38ad7ce8d0f6 100644 --- a/packages/funbox/src/list.ts +++ b/packages/funbox/src/list.ts @@ -268,6 +268,14 @@ const list: Record = { frontendFunctions: ["rememberSettings", "handleKeydown"], name: "read_ahead_hard", }, + tunnel_vision: { + description: "Only see what's near the caret.", + canGetPb: true, + difficultyLevel: 2, + properties: ["changesWordsVisibility", "hasCssFile"], + 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", From 2f0b1ce80ff563d6018bbafb4f2c82cb303f006f Mon Sep 17 00:00:00 2001 From: d1rshan Date: Fri, 15 May 2026 21:51:03 +0530 Subject: [PATCH 2/9] allow update caret only for tunnel vision --- frontend/src/ts/elements/caret.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/ts/elements/caret.ts b/frontend/src/ts/elements/caret.ts index 2010df10e203..45ee745ba7bd 100644 --- a/frontend/src/ts/elements/caret.ts +++ b/frontend/src/ts/elements/caret.ts @@ -557,6 +557,7 @@ export class Caret { private updateWordsCaretPosition(left: number, top: number): void { if (!this.isMainCaret) return; + if (!document.body.classList.contains("fb-tunnel-vision")) return; const style = wordsCache.native.style; const centerX = left - wordsCache.getOffsetLeft() + this.getWidth() / 2; From 96e97031b9cf6fd68e4d6928194de4aa1867ff3f Mon Sep 17 00:00:00 2001 From: d1rshan Date: Fri, 15 May 2026 21:58:29 +0530 Subject: [PATCH 3/9] adjust radius --- frontend/static/funbox/tunnel_vision.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/static/funbox/tunnel_vision.css b/frontend/static/funbox/tunnel_vision.css index 4f7986d46a66..9fa8f681462d 100644 --- a/frontend/static/funbox/tunnel_vision.css +++ b/frontend/static/funbox/tunnel_vision.css @@ -1,6 +1,7 @@ body.fb-tunnel-vision #words { - --tunnel-vision-radius: 4.5em; - --tunnel-vision-softness: 0.75em; + /* 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%), From e9da0bc00733e9fa28dd0973c61997cf75ce5618 Mon Sep 17 00:00:00 2001 From: d1rshan Date: Fri, 15 May 2026 22:09:36 +0530 Subject: [PATCH 4/9] hmm --- frontend/__tests__/test/funbox/funbox-validation.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/__tests__/test/funbox/funbox-validation.spec.ts b/frontend/__tests__/test/funbox/funbox-validation.spec.ts index e3aa0967d0c5..d8c94a6c1f98 100644 --- a/frontend/__tests__/test/funbox/funbox-validation.spec.ts +++ b/frontend/__tests__/test/funbox/funbox-validation.spec.ts @@ -23,8 +23,8 @@ describe("funbox-validation", () => { "rAnDoMcAsE", //changesCapitalisation "nospace", //nospace "plus_one", //toPush: - "read_ahead_easy", //changesWordVisibility - "tunnel_vision", //changesWordVisibility + "read_ahead_easy", //changesWordsVisibility + "tunnel_vision", //changesWordsVisibility "tts", //speaks "layout_mirror", //changesLayout "zipf", //changesWordsFrequency From 07e1eb4c715f9470c53ccd17cd36b78488fca24f Mon Sep 17 00:00:00 2001 From: d1rshan Date: Fri, 15 May 2026 22:18:03 +0530 Subject: [PATCH 5/9] funbox function --- frontend/src/ts/elements/caret.ts | 14 ----- .../src/ts/test/funbox/funbox-functions.ts | 63 +++++++++++++++++++ packages/funbox/src/list.ts | 1 + 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/frontend/src/ts/elements/caret.ts b/frontend/src/ts/elements/caret.ts index 45ee745ba7bd..49abb0fa59cf 100644 --- a/frontend/src/ts/elements/caret.ts +++ b/frontend/src/ts/elements/caret.ts @@ -378,8 +378,6 @@ export class Caret { ...(options.animate && options.animationOptions), }; - this.updateWordsCaretPosition(left, top); - if (options.animate) { this.animatePosition(animateOrPositionOptions); } else { @@ -554,16 +552,4 @@ export class Caret { width, }; } - - private updateWordsCaretPosition(left: number, top: number): void { - if (!this.isMainCaret) return; - if (!document.body.classList.contains("fb-tunnel-vision")) return; - - const style = wordsCache.native.style; - const centerX = left - wordsCache.getOffsetLeft() + this.getWidth() / 2; - const centerY = top - wordsCache.getOffsetTop() + this.getHeight() / 2; - - style.setProperty("--caret-center-x", `${centerX}px`); - style.setProperty("--caret-center-y", `${centerY}px`); - } } diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index dfd315ae4a5e..66f776a94a2c 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -72,6 +72,65 @@ async function readAheadHandleKeydown(event: KeyboardEvent): Promise { } } +let tunnelVisionObserver: MutationObserver | undefined; +let tunnelVisionFrame: number | undefined; + +function updateTunnelVisionCaretPosition(): void { + 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`, + ); +} + +function scheduleTunnelVisionCaretPositionUpdate(): void { + if (tunnelVisionFrame !== undefined) return; + tunnelVisionFrame = requestAnimationFrame(updateTunnelVisionCaretPosition); +} + +function startTunnelVision(): void { + if (tunnelVisionObserver !== undefined) return; + + const caret = document.getElementById("caret"); + if (caret === null) return; + + tunnelVisionObserver = new MutationObserver( + scheduleTunnelVisionCaretPositionUpdate, + ); + tunnelVisionObserver.observe(caret, { + attributes: true, + attributeFilter: ["class", "style"], + }); + window.addEventListener("resize", scheduleTunnelVisionCaretPositionUpdate); + scheduleTunnelVisionCaretPositionUpdate(); +} + +function stopTunnelVision(): void { + tunnelVisionObserver?.disconnect(); + tunnelVisionObserver = undefined; + window.removeEventListener("resize", scheduleTunnelVisionCaretPositionUpdate); + + if (tunnelVisionFrame !== undefined) { + cancelAnimationFrame(tunnelVisionFrame); + tunnelVisionFrame = undefined; + } + + const words = document.getElementById("words"); + words?.style.removeProperty("--caret-center-x"); + words?.style.removeProperty("--caret-center-y"); +} + //todo move to its own file class CharDistribution { public chars: Record; @@ -502,6 +561,10 @@ const list: Partial> = { await readAheadHandleKeydown(event); }, }, + tunnel_vision: { + applyConfig: startTunnelVision, + clearGlobal: stopTunnelVision, + }, memory: { applyConfig(): void { qs("#wordsWrapper")?.hide(); diff --git a/packages/funbox/src/list.ts b/packages/funbox/src/list.ts index 38ad7ce8d0f6..0c1dbedaa003 100644 --- a/packages/funbox/src/list.ts +++ b/packages/funbox/src/list.ts @@ -273,6 +273,7 @@ const list: Record = { canGetPb: true, difficultyLevel: 2, properties: ["changesWordsVisibility", "hasCssFile"], + frontendFunctions: ["applyConfig", "clearGlobal"], name: "tunnel_vision", cssModifications: ["words"], }, From f72117809b69c9ee5ad9572c841be9eea66d5b92 Mon Sep 17 00:00:00 2001 From: d1rshan Date: Fri, 15 May 2026 22:27:20 +0530 Subject: [PATCH 6/9] cleanup --- .../src/ts/test/funbox/funbox-functions.ts | 70 ++++++++++--------- frontend/static/funbox/tunnel_vision.css | 2 +- 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index 66f776a94a2c..7897e99a4dcb 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -99,38 +99,6 @@ function scheduleTunnelVisionCaretPositionUpdate(): void { tunnelVisionFrame = requestAnimationFrame(updateTunnelVisionCaretPosition); } -function startTunnelVision(): void { - if (tunnelVisionObserver !== undefined) return; - - const caret = document.getElementById("caret"); - if (caret === null) return; - - tunnelVisionObserver = new MutationObserver( - scheduleTunnelVisionCaretPositionUpdate, - ); - tunnelVisionObserver.observe(caret, { - attributes: true, - attributeFilter: ["class", "style"], - }); - window.addEventListener("resize", scheduleTunnelVisionCaretPositionUpdate); - scheduleTunnelVisionCaretPositionUpdate(); -} - -function stopTunnelVision(): void { - tunnelVisionObserver?.disconnect(); - tunnelVisionObserver = undefined; - window.removeEventListener("resize", scheduleTunnelVisionCaretPositionUpdate); - - if (tunnelVisionFrame !== undefined) { - cancelAnimationFrame(tunnelVisionFrame); - tunnelVisionFrame = undefined; - } - - const words = document.getElementById("words"); - words?.style.removeProperty("--caret-center-x"); - words?.style.removeProperty("--caret-center-y"); -} - //todo move to its own file class CharDistribution { public chars: Record; @@ -562,8 +530,42 @@ const list: Partial> = { }, }, tunnel_vision: { - applyConfig: startTunnelVision, - clearGlobal: stopTunnelVision, + applyConfig(): void { + if (tunnelVisionObserver !== undefined) return; + + const caret = document.getElementById("caret"); + if (caret === null) return; + + tunnelVisionObserver = new MutationObserver( + scheduleTunnelVisionCaretPositionUpdate, + ); + tunnelVisionObserver.observe(caret, { + attributes: true, + attributeFilter: ["class", "style"], + }); + window.addEventListener( + "resize", + scheduleTunnelVisionCaretPositionUpdate, + ); + scheduleTunnelVisionCaretPositionUpdate(); + }, + clearGlobal(): void { + tunnelVisionObserver?.disconnect(); + tunnelVisionObserver = undefined; + window.removeEventListener( + "resize", + scheduleTunnelVisionCaretPositionUpdate, + ); + + if (tunnelVisionFrame !== undefined) { + cancelAnimationFrame(tunnelVisionFrame); + tunnelVisionFrame = undefined; + } + + const words = document.getElementById("words"); + words?.style.removeProperty("--caret-center-x"); + words?.style.removeProperty("--caret-center-y"); + }, }, memory: { applyConfig(): void { diff --git a/frontend/static/funbox/tunnel_vision.css b/frontend/static/funbox/tunnel_vision.css index 9fa8f681462d..b75814524bf9 100644 --- a/frontend/static/funbox/tunnel_vision.css +++ b/frontend/static/funbox/tunnel_vision.css @@ -1,4 +1,4 @@ -body.fb-tunnel-vision #words { +#words { /* em keeps the visible area proportional to the current typing font size. */ --tunnel-vision-radius: 3em; --tunnel-vision-softness: 0.8em; From 2804b8f0e3a7d4943cbf9b25be503ff77237a070 Mon Sep 17 00:00:00 2001 From: d1rshan Date: Fri, 15 May 2026 22:34:49 +0530 Subject: [PATCH 7/9] more cleanup --- frontend/src/ts/test/funbox/funbox-functions.ts | 5 ----- frontend/static/funbox/tunnel_vision.css | 2 -- 2 files changed, 7 deletions(-) diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index 7897e99a4dcb..ecebda5552dc 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -547,7 +547,6 @@ const list: Partial> = { "resize", scheduleTunnelVisionCaretPositionUpdate, ); - scheduleTunnelVisionCaretPositionUpdate(); }, clearGlobal(): void { tunnelVisionObserver?.disconnect(); @@ -561,10 +560,6 @@ const list: Partial> = { cancelAnimationFrame(tunnelVisionFrame); tunnelVisionFrame = undefined; } - - const words = document.getElementById("words"); - words?.style.removeProperty("--caret-center-x"); - words?.style.removeProperty("--caret-center-y"); }, }, memory: { diff --git a/frontend/static/funbox/tunnel_vision.css b/frontend/static/funbox/tunnel_vision.css index b75814524bf9..6252fa4cda0e 100644 --- a/frontend/static/funbox/tunnel_vision.css +++ b/frontend/static/funbox/tunnel_vision.css @@ -10,6 +10,4 @@ transparent calc(var(--tunnel-vision-radius) + var(--tunnel-vision-softness)) ); - mask-repeat: no-repeat; - mask-size: 100% 100%; } From 980436ee6151641cf2f862299fb13bdc2b1b6498 Mon Sep 17 00:00:00 2001 From: d1rshan Date: Fri, 15 May 2026 22:40:18 +0530 Subject: [PATCH 8/9] merge method --- .../src/ts/test/funbox/funbox-functions.ts | 54 ++++++++----------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index ecebda5552dc..af31481e17c9 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -75,28 +75,26 @@ async function readAheadHandleKeydown(event: KeyboardEvent): Promise { let tunnelVisionObserver: MutationObserver | undefined; let tunnelVisionFrame: number | undefined; -function updateTunnelVisionCaretPosition(): void { - 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`, - ); -} - -function scheduleTunnelVisionCaretPositionUpdate(): void { +function requestCaretPositionUpdate(): void { if (tunnelVisionFrame !== undefined) return; - tunnelVisionFrame = requestAnimationFrame(updateTunnelVisionCaretPosition); + 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 @@ -536,25 +534,17 @@ const list: Partial> = { const caret = document.getElementById("caret"); if (caret === null) return; - tunnelVisionObserver = new MutationObserver( - scheduleTunnelVisionCaretPositionUpdate, - ); + tunnelVisionObserver = new MutationObserver(requestCaretPositionUpdate); tunnelVisionObserver.observe(caret, { attributes: true, attributeFilter: ["class", "style"], }); - window.addEventListener( - "resize", - scheduleTunnelVisionCaretPositionUpdate, - ); + window.addEventListener("resize", requestCaretPositionUpdate); }, clearGlobal(): void { tunnelVisionObserver?.disconnect(); tunnelVisionObserver = undefined; - window.removeEventListener( - "resize", - scheduleTunnelVisionCaretPositionUpdate, - ); + window.removeEventListener("resize", requestCaretPositionUpdate); if (tunnelVisionFrame !== undefined) { cancelAnimationFrame(tunnelVisionFrame); From f2e542aeeb530a97538093634653a4e69a26f47c Mon Sep 17 00:00:00 2001 From: d1rshan Date: Fri, 15 May 2026 22:41:19 +0530 Subject: [PATCH 9/9] update desc --- packages/funbox/src/list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/funbox/src/list.ts b/packages/funbox/src/list.ts index 0c1dbedaa003..cf609f19a1f3 100644 --- a/packages/funbox/src/list.ts +++ b/packages/funbox/src/list.ts @@ -269,7 +269,7 @@ const list: Record = { name: "read_ahead_hard", }, tunnel_vision: { - description: "Only see what's near the caret.", + description: "Only the area around the caret is visible.", canGetPb: true, difficultyLevel: 2, properties: ["changesWordsVisibility", "hasCssFile"],