Skip to content

feat(funbox): add tunnel vision effect (@d1rshan)#7709

Open
d1rshan wants to merge 2 commits intomonkeytypegame:masterfrom
d1rshan:feat/tunnel-vision
Open

feat(funbox): add tunnel vision effect (@d1rshan)#7709
d1rshan wants to merge 2 commits intomonkeytypegame:masterfrom
d1rshan:feat/tunnel-vision

Conversation

@d1rshan
Copy link

@d1rshan d1rshan commented Mar 23, 2026

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.

Copilot AI review requested due to automatic review settings March 23, 2026 17:27
@monkeytypegeorge monkeytypegeorge added frontend User interface or web stuff assets Languages, themes, layouts, etc. packages Changes in local packages labels Mar 23, 2026
@github-actions github-actions bot added the waiting for review Pull requests that require a review before continuing label Mar 23, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_vision to the shared funbox name schema.
  • Register tunnel_vision metadata (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.

Comment on lines +686 to +719
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;
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

assets Languages, themes, layouts, etc. frontend User interface or web stuff packages Changes in local packages waiting for review Pull requests that require a review before continuing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants