feat(funbox): add tunnel vision effect (@d1rshan)#7709
feat(funbox): add tunnel vision effect (@d1rshan)#7709d1rshan wants to merge 2 commits intomonkeytypegame:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new “tunnel vision” funbox that limits visible words to a small radial area around the caret via a CSS mask and caret-position-driven CSS variables.
Changes:
- Add
tunnel_visionto the shared funbox name schema. - Register
tunnel_visionmetadata (properties + frontend hooks) in the funbox list. - Add CSS + frontend funbox function plumbing, plus validation test updates.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/schemas/src/configs.ts | Allows tunnel_vision as a valid FunboxName. |
| packages/funbox/src/list.ts | Adds funbox metadata entry for tunnel_vision (CSS + visibility-changing properties). |
| packages/funbox/test/validation.spec.ts | Adds a compatibility test for multiple changesWordsVisibility funboxes. |
| frontend/static/funbox/tunnel_vision.css | Implements tunnel effect via mask-image/-webkit-mask-image using caret CSS vars. |
| frontend/src/ts/test/funbox/funbox-functions.ts | Implements caret tracking + CSS variable updates for tunnel vision. |
| frontend/tests/test/funbox/funbox-validation.spec.ts | Includes tunnel_vision in mode/funbox validation test cases. |
| 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; | ||
| } |
There was a problem hiding this comment.
tunnel_vision applyGlobalCSS bails if #words missing. applyGlobalCSS is triggered on funbox config changes (can happen off the test page), so this can leave the effect permanently inactive when user later navigates to the test page unless they toggle funbox again. Also, once started it schedules a perpetual requestAnimationFrame loop that won’t stop on page navigation (clearGlobal only runs when funbox removed), which can waste CPU. Suggest: don’t early-return; instead start a loop that waits for #words/#caret to exist (poll via timeout or MutationObserver), updates vars when present, and cancels itself when #words is no longer in DOM / page isn’t test; ensure clearGlobal cancels any pending loop.
| 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; | |
| } | |
| // Ensure any previous observer is cleaned up before starting a new one | |
| const win = window as any; | |
| const existingObserver: MutationObserver | undefined = | |
| win.__tunnelVisionObserver; | |
| if (existingObserver) { | |
| existingObserver.disconnect(); | |
| win.__tunnelVisionObserver = undefined; | |
| } | |
| const startCaretLoop = (): void => { | |
| // Cancel any existing animation frame before starting a new loop | |
| if (tunnelVisionAnimationFrame !== null) { | |
| cancelAnimationFrame(tunnelVisionAnimationFrame); | |
| tunnelVisionAnimationFrame = null; | |
| } | |
| const updateCaretPos = (): void => { | |
| const wordsElem = qs("#words"); | |
| // If #words is missing or detached, stop the loop to avoid CPU waste | |
| if (!wordsElem || !document.body.contains(wordsElem.native)) { | |
| if (tunnelVisionAnimationFrame !== null) { | |
| cancelAnimationFrame(tunnelVisionAnimationFrame); | |
| tunnelVisionAnimationFrame = null; | |
| } | |
| return; | |
| } | |
| 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"; | |
| wordsElem.native.style.setProperty( | |
| "--caret-left", | |
| `calc(${left} + ${marginLeft})`, | |
| ); | |
| wordsElem.native.style.setProperty( | |
| "--caret-top", | |
| `calc(${top} + ${marginTop})`, | |
| ); | |
| } | |
| tunnelVisionAnimationFrame = requestAnimationFrame(updateCaretPos); | |
| }; | |
| tunnelVisionAnimationFrame = requestAnimationFrame(updateCaretPos); | |
| }; | |
| const wordsNow = qs("#words"); | |
| if (wordsNow && document.body.contains(wordsNow.native)) { | |
| // We are on the test page and #words is ready; start immediately | |
| startCaretLoop(); | |
| return; | |
| } | |
| // Wait for #words to appear (e.g. when user navigates to the test page) | |
| const observer = new MutationObserver(() => { | |
| const wordsElem = qs("#words"); | |
| if (wordsElem && document.body.contains(wordsElem.native)) { | |
| const winLocal = window as any; | |
| const currentObserver: MutationObserver | undefined = | |
| winLocal.__tunnelVisionObserver; | |
| if (currentObserver) { | |
| currentObserver.disconnect(); | |
| winLocal.__tunnelVisionObserver = undefined; | |
| } | |
| startCaretLoop(); | |
| } | |
| }); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true, | |
| }); | |
| win.__tunnelVisionObserver = observer; | |
| }, | |
| clearGlobal(): void { | |
| // Cancel any pending animation frame | |
| if (tunnelVisionAnimationFrame !== null) { | |
| cancelAnimationFrame(tunnelVisionAnimationFrame); | |
| tunnelVisionAnimationFrame = null; | |
| } | |
| // Disconnect any active observer waiting for #words | |
| const win = window as any; | |
| const existingObserver: MutationObserver | undefined = | |
| win.__tunnelVisionObserver; | |
| if (existingObserver) { | |
| existingObserver.disconnect(); | |
| win.__tunnelVisionObserver = undefined; | |
| } |
Adds a new funbox effect 'tunnel vision' - only a small radial area around the caret will be visible.
recording-2026-03-23_22-40-58.mp4
There are a few things left to improve, but I’d love feedback on the effect before i continue to work on it.