From 001da7b99213516eeb4c0aee1d1c917275d30518 Mon Sep 17 00:00:00 2001 From: d1rshan Date: Mon, 23 Mar 2026 18:40:09 +0530 Subject: [PATCH] feat(funbox): add tunnel vision effect (@d1rshan) --- .../test/funbox/funbox-validation.spec.ts | 1 + .../src/ts/test/funbox/funbox-functions.ts | 45 +++++++++++++++++++ frontend/static/funbox/tunnel_vision.css | 16 +++++++ packages/funbox/__test__/validation.spec.ts | 20 +++++++++ packages/funbox/src/list.ts | 8 ++++ packages/schemas/src/configs.ts | 1 + 6 files changed, 91 insertions(+) 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/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index 1c65da0b870a..e81c0b3fedc8 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -166,6 +166,8 @@ export class PolyglotWordset extends Wordset { } } +let tunnelVisionAnimationFrame: number | null = null; + const list: Partial> = { "58008": { getWord(): string { @@ -679,6 +681,49 @@ const list: Partial> = { return word.toUpperCase(); }, }, + tunnel_vision: { + applyGlobalCSS(): void { + const words = qs("#words"); + if (!words) return; + + const updateCaretPos = (): void => { + const caretElem = qs("#caret"); + if (caretElem !== null) { + const caretStyle = caretElem.getStyle(); + const left = caretStyle.left || "0px"; + const top = caretStyle.top || "0px"; + const marginLeft = caretStyle.marginLeft || "0px"; + const marginTop = caretStyle.marginTop || "0px"; + + words.native.style.setProperty( + "--caret-left", + `calc(${left} + ${marginLeft})`, + ); + words.native.style.setProperty( + "--caret-top", + `calc(${top} + ${marginTop})`, + ); + } + tunnelVisionAnimationFrame = requestAnimationFrame(updateCaretPos); + }; + + if (tunnelVisionAnimationFrame !== null) { + cancelAnimationFrame(tunnelVisionAnimationFrame); + } + updateCaretPos(); + }, + clearGlobal(): void { + if (tunnelVisionAnimationFrame !== null) { + cancelAnimationFrame(tunnelVisionAnimationFrame); + tunnelVisionAnimationFrame = null; + } + const words = qs("#words"); + if (words) { + words.native.style.removeProperty("--caret-left"); + words.native.style.removeProperty("--caret-top"); + } + }, + }, polyglot: { async withWords(_words) { const promises = Config.customPolyglot.map(async (language) => diff --git a/frontend/static/funbox/tunnel_vision.css b/frontend/static/funbox/tunnel_vision.css new file mode 100644 index 000000000000..144fd4a35f04 --- /dev/null +++ b/frontend/static/funbox/tunnel_vision.css @@ -0,0 +1,16 @@ +#words { + mask-image: radial-gradient( + circle 150px at var(--caret-left) var(--caret-top), + black 0%, + black 50%, + transparent 100% + ); + -webkit-mask-image: radial-gradient( + circle 150px at var(--caret-left) var(--caret-top), + black 0%, + black 50%, + transparent 100% + ); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; +} diff --git a/packages/funbox/__test__/validation.spec.ts b/packages/funbox/__test__/validation.spec.ts index de27659acbc0..7c919ff8600f 100644 --- a/packages/funbox/__test__/validation.spec.ts +++ b/packages/funbox/__test__/validation.spec.ts @@ -115,6 +115,26 @@ describe("validation", () => { true, ); }); + + it("should reject multiple word visibility funboxes", () => { + //GIVEN + getFunboxMock.mockReturnValueOnce([ + { + name: "plus_one", + properties: ["changesWordsVisibility"], + } as FunboxMetadata, + { + name: "tunnel_vision", + properties: ["changesWordsVisibility"], + } as FunboxMetadata, + ]); + + //WHEN / THEN + expect(Validation.checkCompatibility(["plus_one", "tunnel_vision"])).toBe( + false, + ); + }); + describe("should validate two funboxes modifying the wordset", () => { const testCases = [ { diff --git a/packages/funbox/src/list.ts b/packages/funbox/src/list.ts index 19cd30abca89..06f818c9339f 100644 --- a/packages/funbox/src/list.ts +++ b/packages/funbox/src/list.ts @@ -481,6 +481,14 @@ const list: Record = { difficultyLevel: 0, name: "no_quit", }, + tunnel_vision: { + name: "tunnel_vision", + description: "Only the area around the caret is visible.", + canGetPb: true, + difficultyLevel: 2, + properties: ["hasCssFile", "changesWordsVisibility"], + frontendFunctions: ["applyGlobalCSS", "clearGlobal"], + }, }; export function getObject(): Record { diff --git a/packages/schemas/src/configs.ts b/packages/schemas/src/configs.ts index 77c257a54834..037400ed9162 100644 --- a/packages/schemas/src/configs.ts +++ b/packages/schemas/src/configs.ts @@ -321,6 +321,7 @@ export const FunboxNameSchema = z.enum([ "asl", "rot13", "no_quit", + "tunnel_vision", ]); export type FunboxName = z.infer;