Transforms GeoGuessr panoramas into a live, fully customizable ASCII text art display with native retro filter controls.
// ==UserScript==
// @name GeoASCII
// @description Transforms GeoGuessr panoramas into a live, fully customizable ASCII text art display with native retro filter controls.
// @version 1.13.0
// @author maxtmiller
// @match https://www.geoguessr.com/*
// @run-at document-start
// @license MIT
// @icon https://raw.githubusercontent.com/maxtmiller/GeoASCII/main/assets/geoascii-icon-32.png
// @namespace https://github.com/maxtmiller/GeoASCII
// @grant none
// @tag geoguessr
// @tag games
// @tag ascii
// ==/UserScript==
// Default Baseline Configurations
const DEFAULT_RESOLUTION = 150;
const DEFAULT_CONTRAST = 1.0;
const DEFAULT_SATURATION = 1.0;
const DEFAULT_BRIGHTNESS = 1.0;
const DEFAULT_NOISE = 0;
const DEFAULT_BLUR = 0.0;
const DEFAULT_TINT_HEX = "#bc13fe";
const DEFAULT_PALETTE_SIZE = 10;
const DEFAULT_CMD_COLOURS = false;
let asciiEnabled = true;
let charResolution = DEFAULT_RESOLUTION;
let contrastSetting = DEFAULT_CONTRAST;
let saturationSetting = DEFAULT_SATURATION;
let brightnessSetting = DEFAULT_BRIGHTNESS;
let noiseSetting = DEFAULT_NOISE;
let blurSetting = DEFAULT_BLUR;
let paletteSizeSetting = DEFAULT_PALETTE_SIZE;
let resolutionSettingEnabled = true;
let paletteSettingEnabled = true;
let brightnessSettingEnabled = true;
let contrastSettingEnabled = true;
let saturationSettingEnabled = true;
let noiseSettingEnabled = true;
let blurSettingEnabled = true;
// Feature Toggles
let solidPixelMode = false;
let edgeDetectionMode = false;
let colorInversionMode = false;
let scanlineMode = false;
let thermalMode = false;
let tintMode = false;
let cmdColoursMode = DEFAULT_CMD_COLOURS;
let customTintHex = DEFAULT_TINT_HEX;
// --- Performance System ---
let cachedGameContainer = null;
let cachedWebGlCanvas = null;
let cachedIsGame = false;
let lifecycleCheckInterval = null;
let forceRender = true;
let renderQueued = false;
let cameraMoving = true;
let lastMovementTime = 0;
let userInteracting = false;
let interactionTimeout = null;
let mapInteractionUntil = 0;
let mapPointerDown = false;
let mapPointerId = null;
let activeWebGlCanvas = null;
let waitingForFirstAsciiFrame = true;
let transitionBlockedCanvas = null;
let transitionBlockedFrameSignature = "";
let transitionSameCanvasBlockUntil = 0;
let transitionBlockUntil = 0;
const ACTIVE_FPS = 15;
const MOVEMENT_FPS = 30;
const FIRST_FRAME_FPS = 60;
const IDLE_FPS = 2;
const CHANGE_CHECK_FPS = 20;
const MAP_WHEEL_PAUSE_MS = 180;
const MAP_POINTER_RELEASE_PAUSE_MS = 120;
const TRANSITION_SAME_CANVAS_BLOCK_MS = 900;
const TRANSITION_BLOCK_FALLBACK_MS = 1200;
let currentFpsInterval = 1000 / ACTIVE_FPS;
let lastChangeCheckTime = 0;
const MASTER_PALETTE = '`,_!;|\\\"~^lr[](\\/L)>t<vTz?icf1{sIxYjJno}CZyVwmSXRqM$O9&NW0Q';
const MASTER_PALETTE_BASE = '@%#*+=-:. ';
let cachedPaletteSize = null;
let cachedPalette = "";
function getDynamicPalette() {
const effectivePaletteSize = paletteSettingEnabled ? paletteSizeSetting : DEFAULT_PALETTE_SIZE;
if (cachedPaletteSize === effectivePaletteSize) return cachedPalette;
if (effectivePaletteSize >= MASTER_PALETTE.length) {
cachedPaletteSize = effectivePaletteSize;
cachedPalette = MASTER_PALETTE;
return cachedPalette;
}
let result = "";
const step = (MASTER_PALETTE.length - 1) / (effectivePaletteSize - 1);
for (let i = 0; i < effectivePaletteSize; i++) {
result += MASTER_PALETTE_BASE[i];
}
if (effectivePaletteSize > 10) {
for (let i = 0; i < effectivePaletteSize - 10; i++) {
const index = Math.round(i * step);
result += MASTER_PALETTE[index];
}
}
cachedPaletteSize = effectivePaletteSize;
cachedPalette = result;
return result;
}
let lastKnownWebGLWidth = 0;
let lastKnownWebGLHeight = 0;
let observedPanoramaContainer = null;
let containerMutationObserver = null;
let panelIsOpen = false;
// --- Core WebGL Interception Engine ---
const originalGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function (type, attributes) {
if (type === 'webgl' || type === 'webgl2') {
attributes = attributes || {};
attributes.preserveDrawingBuffer = true;
this.dataset.isInterceptedWebgl = "true";
}
return originalGetContext.call(this, type, attributes);
};
// Inject Styles
const styleElement = document.createElement("style");
styleElement.innerHTML = `
#ascii-art-canvas {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
background-color: #05010a !important;
overflow: hidden !important;
z-index: 2147483000 !important;
pointer-events: none !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
}
#ascii-display-canvas {
transform-origin: center center !important;
image-rendering: pixelated !important;
}
#ascii-control-panel {
position: absolute;
bottom: max(20px, 10vh);
left: 20px;
z-index: 99999;
background: rgba(14, 5, 24, 0.95);
border: 1px solid rgba(176, 38, 255, 0.3);
border-radius: 14px;
padding: 16px 8px 16px 16px;
color: #f3e8ff;
width: min(340px, 60vw);
max-height: 75vh;
display: none;
flex-direction: column;
box-shadow: 0 12px 40px rgba(0,0,0,0.7);
backdrop-filter: blur(6px);
font-family: Neo Sans, sans-serif, Helvetica, Arial;
}
.ascii-panel-content {
overflow-y: auto;
overflow-x: hidden;
flex-grow: 1;
padding-right: 12px;
}
.ascii-panel-content::-webkit-scrollbar { width: 4px; }
.ascii-panel-content::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.02); }
.ascii-panel-content::-webkit-scrollbar-thumb { background: rgba(176, 38, 255, 0.4); border-radius: 4px; }
[class*="styles_friendChatButton__"] { width: auto !important; height: auto !important; display: inline-flex !important; flex-direction: column !important; align-items: flex-start !important; }
[class*="styles_friendChatButton__"] > :not(.native-ascii-btn) { order: 1 !important; }
.native-ascii-btn {
order: 2 !important;
background: rgba(0, 0, 0, 0.6) !important;
color: #b026ff !important;
border: 1px solid rgba(176, 38, 255, 0.4) !important;
border-radius: 24px !important;
min-width: 110px !important;
height: 40px !important;
font-weight: bold !important;
font-size: 13px !important;
text-transform: uppercase !important;
letter-spacing: 0.8px !important;
cursor: pointer !important;
backdrop-filter: blur(4px) !important;
box-shadow: 0 0 15px rgba(176, 38, 255, 0.3), inset 0 0 10px rgba(176, 38, 255, 0.1) !important;
transition: all 0.2s ease-in-out !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 16px !important;
margin: 4px 0 !important;
}
.native-ascii-btn:hover {
background: rgba(0, 0, 0, 0.8) !important;
color: #c55eff !important;
border-color: rgba(176, 38, 255, 0.8) !important;
box-shadow: 0 0 25px rgba(176, 38, 255, 0.6), inset 0 0 10px rgba(176, 38, 255, 0.2) !important;
}
.ascii-btn-row { display: flex; gap: 8px; margin-top: 12px; padding-right: 8px; }
.ascii-action-btn { flex: 1; background: rgba(176, 38, 255, 0.2); border: 1px solid rgba(176, 38, 255, 0.5); color: #f3e8ff; padding: 8px 4px; border-radius: 6px; font-weight: bold; font-size: 10px; letter-spacing: 0.5px; text-transform: uppercase; cursor: pointer; transition: all 0.15s ease-in-out; }
.ascii-action-btn:hover { background: rgba(176, 38, 255, 0.4); border-color: #c55eff; box-shadow: 0 0 10px rgba(176, 38, 255, 0.4); }
.ascii-action-btn.reset-variant { background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.2); }
.ascii-action-btn.reset-variant:hover { background: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.4); box-shadow: 0 0 10px rgba(255, 255, 255, 0.1); }
.panel-title { font-size: 13px; font-weight: bold; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 12px; color: #c55eff; border-bottom: 1px solid rgba(176, 38, 255, 0.2); padding-bottom: 5px; flex-shrink: 0; padding-right: 8px; }
.ascii-slider-group { margin-bottom: 10px; }
.ascii-slider-group label { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 4px; letter-spacing: 0.5px; gap: 10px; }
.ascii-slider-control { display: flex; align-items: center; gap: 8px; }
.ascii-slider { width: 100%; accent-color: #b026ff; cursor: pointer; }
.ascii-slider-control .ascii-slider { flex: 1; min-width: 0; }
.ascii-toggle-group { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; font-size: 11px; }
.ascii-checkbox { accent-color: #b026ff; cursor: pointer; width: 16px; height: 16px; }
.ascii-color-picker-wrapper { display: flex; align-items: center; gap: 8px; }
.ascii-color-picker { background: none; border: none; width: 28px; height: 24px; cursor: pointer; padding: 0; }
.ascii-details { margin-top: 12px; border-top: 1px solid rgba(176, 38, 255, 0.2); padding-top: 6px; }
.ascii-summary { font-size: 11px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; color: #c55eff; cursor: pointer; padding: 6px 0; user-select: none; outline: none; display: flex; justify-content: space-between; align-items: center; }
.ascii-summary::-webkit-details-marker { display: none; }
.ascii-summary::after { content: '▼'; font-size: 9px; transition: transform 0.2s ease; color: rgba(176, 38, 255, 0.7); }
.ascii-details[open] .ascii-summary::after { transform: rotate(180deg); }
`;
function appendStyleElement() {
const styleParent = document.head || document.documentElement;
if (!styleParent || styleElement.isConnected) return;
styleParent.appendChild(styleElement);
}
appendStyleElement();
document.addEventListener('DOMContentLoaded', appendStyleElement, { once: true });
let asciiWrapper = null;
let displayCanvas = null;
let displayCtx = null;
let captureCanvas = document.createElement('canvas');
let captureCtx = captureCanvas.getContext('2d', { willReadFrequently: true });
let probeCanvas = document.createElement('canvas');
let probeCtx = probeCanvas.getContext('2d', { willReadFrequently: true });
let previousFrameSignature = "";
const CMD_COLOURS = [
[0, 0, 0],
[0, 0, 128],
[0, 128, 0],
[0, 128, 128],
[128, 0, 0],
[128, 0, 128],
[128, 128, 0],
[192, 192, 192],
[128, 128, 128],
[0, 0, 255],
[0, 255, 0],
[0, 255, 255],
[255, 0, 0],
[255, 0, 255],
[255, 255, 0],
[255, 255, 255]
];
const cmdColourStringCache = CMD_COLOURS.map(([r, g, b]) => `rgb(${r},${g},${b})`);
const rgbStringCache = new Array(4096);
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 188, g: 19, b: 254 };
}
function commandPromptColourString(r, g, b) {
let bestIndex = 0;
let bestDistance = Infinity;
for (let i = 0; i < CMD_COLOURS.length; i++) {
const colour = CMD_COLOURS[i];
const dr = r - colour[0];
const dg = g - colour[1];
const db = b - colour[2];
const distance = dr * dr + dg * dg + db * db;
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = i;
}
}
return cmdColourStringCache[bestIndex];
}
function quantizedRgbString(r, g, b) {
const rq = Math.max(0, Math.min(15, r >> 4));
const gq = Math.max(0, Math.min(15, g >> 4));
const bq = Math.max(0, Math.min(15, b >> 4));
const key = (rq << 8) | (gq << 4) | bq;
let value = rgbStringCache[key];
if (!value) {
value = `rgb(${rq * 17},${gq * 17},${bq * 17})`;
rgbStringCache[key] = value;
}
return value;
}
function renderColourString(r, g, b) {
return cmdColoursMode
? commandPromptColourString(r, g, b)
: quantizedRgbString(r, g, b);
}
function effectiveResolution() {
return resolutionSettingEnabled ? charResolution : DEFAULT_RESOLUTION;
}
function effectiveBrightness() {
return brightnessSettingEnabled ? brightnessSetting : DEFAULT_BRIGHTNESS;
}
function effectiveContrast() {
return contrastSettingEnabled ? contrastSetting : DEFAULT_CONTRAST;
}
function effectiveSaturation() {
return saturationSettingEnabled ? saturationSetting : DEFAULT_SATURATION;
}
function effectiveNoise() {
return noiseSettingEnabled ? noiseSetting : DEFAULT_NOISE;
}
function effectiveBlur() {
return blurSettingEnabled ? blurSetting : DEFAULT_BLUR;
}
function markRendererDirty() {
forceRender = true;
previousFrameSignature = "";
}
function closestElement(target, selector) {
if (!target) return null;
const element = target.nodeType === Node.ELEMENT_NODE
? target
: target.parentElement;
return element ? element.closest(selector) : null;
}
function isVisibleElement(element) {
if (!element) return false;
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return (
rect.width > 20 &&
rect.height > 20 &&
rect.bottom > 0 &&
rect.right > 0 &&
rect.top < window.innerHeight &&
rect.left < window.innerWidth &&
style.display !== 'none' &&
style.visibility !== 'hidden' &&
Number(style.opacity || 1) > 0.05
);
}
function isResultsScreenActive() {
return !!(
document.querySelector('[data-qa="result-layout"]') ||
document.querySelector('[data-qa="round-result"]') ||
document.querySelector('[data-qa="final-results"]') ||
document.querySelector('[class*="result-layout"]') ||
document.querySelector('[class*="game-summary"]')
);
}
function isGuessMapElement(element) {
const targetElement = closestElement(element, '*');
if (
!targetElement ||
targetElement.closest('#ascii-control-panel') ||
targetElement.closest('.native-ascii-btn')
) {
return false;
}
const mapElement = targetElement.closest([
'[data-qa="guess-map"]',
'[data-qa="guess-map-canvas"]',
'[data-qa="guess-map__canvas"]',
'[class*="guess-map"]',
'[class*="guessMap"]',
'[class*="mapboxgl-map"]',
'.mapboxgl-canvas',
'.leaflet-container'
].join(','));
if (!mapElement || !isVisibleElement(mapElement)) return false;
if (cachedGameContainer && cachedGameContainer.contains(mapElement)) return false;
return true;
}
function isRoundTransitionControl(element) {
const targetElement = closestElement(element, '*');
if (
!targetElement ||
targetElement.closest('#ascii-control-panel') ||
targetElement.closest('.native-ascii-btn')
) {
return false;
}
const control = targetElement.closest('button, a, [role="button"]');
if (!control || !isVisibleElement(control)) return false;
const combinedText = [
control.textContent,
control.getAttribute('aria-label'),
control.getAttribute('data-qa'),
control.className
].join(' ').toLowerCase();
return (
/\b(next|play)\s+round\b/.test(combinedText) ||
/\bnext\b/.test(combinedText) && /\bround\b/.test(combinedText) ||
/\b(play\s+next|start\s+game|play\s+again|new\s+round)\b/.test(combinedText)
);
}
function handlePossibleRoundTransition(event) {
if (!cachedIsGame && !isGameRoute()) return;
if (!isRoundTransitionControl(event.target)) return;
resetForPanoramaTransition();
coverPanoramaIfPossible();
requestImmediateRender();
}
function isTextEntryElement(element) {
const targetElement = closestElement(element, '*');
return !!(
targetElement &&
(
targetElement.isContentEditable ||
targetElement.closest('input, textarea, select, [contenteditable="true"]')
)
);
}
function handlePossibleRoundTransitionKey(event) {
if (event.key !== ' ' && event.key !== 'Enter') return;
if (!cachedIsGame && !isGameRoute()) return;
if (isTextEntryElement(event.target)) return;
const activeElement = document.activeElement;
const shouldReset =
isRoundTransitionControl(activeElement) ||
isRoundTransitionControl(event.target) ||
isResultsScreenActive();
if (!shouldReset) return;
resetForPanoramaTransition();
coverPanoramaIfPossible();
requestImmediateRender();
}
function markMapInteraction(event, durationMs) {
if (isGuessMapElement(event.target)) {
mapInteractionUntil = performance.now() + durationMs;
}
}
function isMapInteractionActive() {
return mapPointerDown || performance.now() < mapInteractionUntil;
}
function beginMapPointer(event) {
if (!isGuessMapElement(event.target)) return;
mapPointerDown = true;
mapPointerId = event.pointerId ?? 'touch';
mapInteractionUntil = 0;
}
function endMapPointer(event) {
if (!mapPointerDown) return;
if (event && mapPointerId !== null && event.pointerId !== undefined && event.pointerId !== mapPointerId) return;
mapPointerDown = false;
mapPointerId = null;
mapInteractionUntil = performance.now() + MAP_POINTER_RELEASE_PAUSE_MS;
}
function handleMapWheel(event) {
markMapInteraction(event, MAP_WHEEL_PAUSE_MS);
}
function cancelMapInteraction() {
mapPointerDown = false;
mapPointerId = null;
mapInteractionUntil = 0;
}
function beginInteraction(event) {
if (event && isGuessMapElement(event.target)) {
return;
}
userInteracting = true;
cameraMoving = true;
lastMovementTime = performance.now();
clearTimeout(interactionTimeout);
interactionTimeout = setTimeout(() => {
userInteracting = false;
}, 160);
}
function canvasFrameSignature(webGlCanvas) {
if (!probeCtx || !webGlCanvas) return "";
const probeWidth = 24;
const probeHeight = 14;
if (probeCanvas.width !== probeWidth || probeCanvas.height !== probeHeight) {
probeCanvas.width = probeWidth;
probeCanvas.height = probeHeight;
}
try {
probeCtx.drawImage(webGlCanvas, 0, 0, probeWidth, probeHeight);
const sample = probeCtx.getImageData(0, 0, probeWidth, probeHeight).data;
let signature = "";
for (let i = 0; i < sample.length; i += 32) {
signature += String.fromCharCode(
sample[i] >> 3,
sample[i + 1] >> 3,
sample[i + 2] >> 3
);
}
return signature;
} catch (e) {
return "";
}
}
function frameSignatureChanged(webGlCanvas) {
const signature = canvasFrameSignature(webGlCanvas);
if (!signature) return true;
const changed = signature !== previousFrameSignature;
previousFrameSignature = signature;
return changed;
}
function generateGaussianNoise(stdDev) {
if (stdDev === 0) return 0;
let u1 = Math.random();
let u2 = Math.random();
if(u1 === 0) u1 = 0.0001;
let randStdNormal = Math.sqrt(-2.0 * Math.log(u1)) * Math.sin(2.0 * Math.PI * u2);
return randStdNormal * stdDev;
}
function clearAndForceResizeBuffers() {
captureCanvas.width = 0;
captureCanvas.height = 0;
previousFrameSignature = "";
forceRender = true;
}
function resetForPanoramaTransition() {
const currentCanvas = activeWebGlCanvas || cachedWebGlCanvas;
const now = performance.now();
transitionBlockedCanvas = currentCanvas;
transitionBlockedFrameSignature = canvasFrameSignature(currentCanvas);
transitionSameCanvasBlockUntil = now + TRANSITION_SAME_CANVAS_BLOCK_MS;
transitionBlockUntil = now + TRANSITION_BLOCK_FALLBACK_MS;
lastKnownWebGLWidth = 0;
lastKnownWebGLHeight = 0;
cachedWebGlCanvas = null;
activeWebGlCanvas = null;
clearAndForceResizeBuffers();
beginFirstFrameMode();
clearDisplayedAsciiFrame();
}
function beginFirstFrameMode() {
waitingForFirstAsciiFrame = true;
forceRender = true;
lastFrameTime = 0;
currentFpsInterval = 1000 / FIRST_FRAME_FPS;
}
function clearDisplayedAsciiFrame() {
if (!displayCanvas || !displayCtx) return;
displayCtx.setTransform(1, 0, 0, 1, 0, 0);
displayCtx.fillStyle = '#05010a';
displayCtx.fillRect(0, 0, displayCanvas.width, displayCanvas.height);
}
function imageDataHasVisibleContent(data) {
let total = 0;
const step = Math.max(4, Math.floor(data.length / 256) & ~3);
for (let i = 0; i < data.length; i += step) {
total += data[i] + data[i + 1] + data[i + 2];
if (total > 600) return true;
}
return false;
}
function nodeContainsPanoramaCanvas(node) {
if (!(node instanceof Element)) return false;
return !!(
node.matches('canvas, .widget-scene-canvas, [data-is-intercepted-webgl="true"]') ||
node.querySelector('canvas, .widget-scene-canvas, [data-is-intercepted-webgl="true"]')
);
}
function mutationTouchesPanoramaCanvas(mutation) {
return (
[...mutation.addedNodes].some(nodeContainsPanoramaCanvas) ||
[...mutation.removedNodes].some(nodeContainsPanoramaCanvas)
);
}
function randomizeShaders() {
charResolution = Math.floor(Math.random() * (300 - 80 + 1)) + 80;
paletteSizeSetting = Math.floor(Math.random() * (MASTER_PALETTE.length - 4 + 1)) + 4;
brightnessSetting = parseFloat((Math.random() * (1.4 - 0.6) + 0.6).toFixed(2));
contrastSetting = parseFloat((Math.random() * (2.2 - 0.6) + 0.6).toFixed(1));
saturationSetting = parseFloat((Math.random() * (2.5 - 0.0) + 0.0).toFixed(1));
noiseSetting = Math.floor(Math.random() * 5) * 10;
blurSetting = parseFloat((Math.random() * 1.5).toFixed(1));
resolutionSettingEnabled = true;
paletteSettingEnabled = true;
brightnessSettingEnabled = true;
contrastSettingEnabled = true;
saturationSettingEnabled = true;
noiseSettingEnabled = true;
blurSettingEnabled = true;
const modeRoll = Math.random();
solidPixelMode = modeRoll > 0.80;
edgeDetectionMode = modeRoll > 0.60 && modeRoll <= 0.80;
thermalMode = modeRoll > 0.40 && modeRoll <= 0.60;
colorInversionMode = Math.random() > 0.85;
scanlineMode = Math.random() > 0.40;
tintMode = Math.random() > 0.50 || thermalMode === false;
cmdColoursMode = Math.random() > 0.5;
customTintHex = "#" + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
syncGlobalVariablesToUi();
clearAndForceResizeBuffers();
markRendererDirty();
}
function resetShaders() {
charResolution = DEFAULT_RESOLUTION;
paletteSizeSetting = DEFAULT_PALETTE_SIZE;
contrastSetting = DEFAULT_CONTRAST;
saturationSetting = DEFAULT_SATURATION;
brightnessSetting = DEFAULT_BRIGHTNESS;
noiseSetting = DEFAULT_NOISE;
blurSetting = DEFAULT_BLUR;
resolutionSettingEnabled = true;
paletteSettingEnabled = true;
brightnessSettingEnabled = true;
contrastSettingEnabled = true;
saturationSettingEnabled = true;
noiseSettingEnabled = true;
blurSettingEnabled = true;
solidPixelMode = false;
edgeDetectionMode = false;
colorInversionMode = false;
scanlineMode = false;
thermalMode = false;
tintMode = false;
cmdColoursMode = DEFAULT_CMD_COLOURS;
customTintHex = DEFAULT_TINT_HEX;
syncGlobalVariablesToUi();
clearAndForceResizeBuffers();
markRendererDirty();
}
function syncGlobalVariablesToUi() {
if (!document.getElementById('ascii-control-panel')) return;
document.getElementById('res-enable').checked = resolutionSettingEnabled;
document.getElementById('res-slider').value = charResolution;
document.getElementById('res-slider').disabled = !resolutionSettingEnabled;
document.getElementById('res-val').innerText = effectiveResolution();
document.getElementById('palette-enable').checked = paletteSettingEnabled;
document.getElementById('palette-slider').value = paletteSizeSetting;
document.getElementById('palette-slider').disabled = !paletteSettingEnabled;
document.getElementById('palette-val').innerText = (paletteSettingEnabled ? paletteSizeSetting : DEFAULT_PALETTE_SIZE) + " Chars";
document.getElementById('brightness-enable').checked = brightnessSettingEnabled;
document.getElementById('brightness-slider').value = brightnessSetting;
document.getElementById('brightness-slider').disabled = !brightnessSettingEnabled;
document.getElementById('brightness-val').innerText = Math.round(effectiveBrightness() * 100) + "%";
document.getElementById('contrast-enable').checked = contrastSettingEnabled;
document.getElementById('contrast-slider').value = contrastSetting;
document.getElementById('contrast-slider').disabled = !contrastSettingEnabled;
document.getElementById('contrast-val').innerText = effectiveContrast().toFixed(1);
document.getElementById('sat-enable').checked = saturationSettingEnabled;
document.getElementById('sat-slider').value = saturationSetting;
document.getElementById('sat-slider').disabled = !saturationSettingEnabled;
document.getElementById('sat-val').innerText = effectiveSaturation().toFixed(1);
document.getElementById('noise-enable').checked = noiseSettingEnabled;
document.getElementById('noise-slider').value = noiseSetting;
document.getElementById('noise-slider').disabled = !noiseSettingEnabled;
document.getElementById('noise-val').innerText = effectiveNoise();
document.getElementById('blur-enable').checked = blurSettingEnabled;
document.getElementById('blur-slider').value = blurSetting;
document.getElementById('blur-slider').disabled = !blurSettingEnabled;
document.getElementById('blur-val').innerText = effectiveBlur().toFixed(1) + "px";
document.getElementById('solid-toggle').checked = solidPixelMode;
document.getElementById('edge-toggle').checked = edgeDetectionMode;
document.getElementById('invert-toggle').checked = colorInversionMode;
document.getElementById('scanline-toggle').checked = scanlineMode;
document.getElementById('thermal-toggle').checked = thermalMode;
document.getElementById('cmd-colours-toggle').checked = cmdColoursMode;
document.getElementById('tint-toggle').checked = tintMode;
document.getElementById('tint-picker').value = customTintHex;
}
function createUiPanel() {
let panel = document.getElementById('ascii-control-panel');
if (!panel) {
panel = document.createElement('div');
panel.id = 'ascii-control-panel';
panel.innerHTML = `
<div class="panel-title">🎛️ Master Controls</div>
<div class="ascii-panel-content">
<div class="ascii-toggle-group" style="margin-bottom: 12px; background: rgba(176,38,255,0.15); padding: 8px; border-radius: 6px;">
<span style="font-weight: bold; color: #c55eff;">Enable ASCII Shaders</span>
<input type="checkbox" id="master-ascii-toggle" class="ascii-checkbox" ${asciiEnabled ? 'checked' : ''}>
</div>
<div class="ascii-slider-group">
<label><span>Resolution</span><span id="res-val">${effectiveResolution()}</span></label>
<div class="ascii-slider-control"><input type="checkbox" id="res-enable" class="ascii-checkbox" ${resolutionSettingEnabled ? 'checked' : ''}><input type="range" id="res-slider" class="ascii-slider" min="50" max="350" value="${charResolution}"></div>
</div>
<div class="ascii-slider-group">
<label><span>Palette Details</span><span id="palette-val">${paletteSettingEnabled ? paletteSizeSetting : DEFAULT_PALETTE_SIZE} Chars</span></label>
<div class="ascii-slider-control"><input type="checkbox" id="palette-enable" class="ascii-checkbox" ${paletteSettingEnabled ? 'checked' : ''}><input type="range" id="palette-slider" class="ascii-slider" min="4" max="${MASTER_PALETTE.length}" step="1" value="${paletteSizeSetting}"></div>
</div>
<div class="ascii-slider-group">
<label><span>Brightness</span><span id="brightness-val">${Math.round(effectiveBrightness() * 100)}%</span></label>
<div class="ascii-slider-control"><input type="checkbox" id="brightness-enable" class="ascii-checkbox" ${brightnessSettingEnabled ? 'checked' : ''}><input type="range" id="brightness-slider" class="ascii-slider" min="0.5" max="1.5" step="0.05" value="${brightnessSetting}"></div>
</div>
<div class="ascii-slider-group">
<label><span>Contrast</span><span id="contrast-val">${effectiveContrast().toFixed(1)}</span></label>
<div class="ascii-slider-control"><input type="checkbox" id="contrast-enable" class="ascii-checkbox" ${contrastSettingEnabled ? 'checked' : ''}><input type="range" id="contrast-slider" class="ascii-slider" min="0.5" max="2.5" step="0.1" value="${contrastSetting}"></div>
</div>
<div class="ascii-slider-group">
<label><span>Saturation</span><span id="sat-val">${effectiveSaturation().toFixed(1)}</span></label>
<div class="ascii-slider-control"><input type="checkbox" id="sat-enable" class="ascii-checkbox" ${saturationSettingEnabled ? 'checked' : ''}><input type="range" id="sat-slider" class="ascii-slider" min="0.0" max="3.0" step="0.1" value="${saturationSetting}"></div>
</div>
<div class="ascii-slider-group">
<label><span>Gaussian Noise</span><span id="noise-val">${effectiveNoise()}</span></label>
<div class="ascii-slider-control"><input type="checkbox" id="noise-enable" class="ascii-checkbox" ${noiseSettingEnabled ? 'checked' : ''}><input type="range" id="noise-slider" class="ascii-slider" min="0" max="80" step="5" value="${noiseSetting}"></div>
</div>
<div class="ascii-slider-group">
<label><span>Pre-Blur</span><span id="blur-val">${effectiveBlur().toFixed(1) + "px"}</span></label>
<div class="ascii-slider-control"><input type="checkbox" id="blur-enable" class="ascii-checkbox" ${blurSettingEnabled ? 'checked' : ''}><input type="range" id="blur-slider" class="ascii-slider" min="0" max="5" step="0.1" value="${blurSetting}"></div>
</div>
<details class="ascii-details">
<summary class="ascii-summary">Extra Settings</summary>
<div class="ascii-toggle-group"><span>Solid Pixels (█)</span><input type="checkbox" id="solid-toggle" class="ascii-checkbox"></div>
<div class="ascii-toggle-group"><span>Edge Detection</span><input type="checkbox" id="edge-toggle" class="ascii-checkbox"></div>
<div class="ascii-toggle-group"><span>Invert Colors</span><input type="checkbox" id="invert-toggle" class="ascii-checkbox"></div>
<div class="ascii-toggle-group"><span>CRT Scanlines</span><input type="checkbox" id="scanline-toggle" class="ascii-checkbox"></div>
<div class="ascii-toggle-group"><span>Thermal / Heatmap</span><input type="checkbox" id="thermal-toggle" class="ascii-checkbox"></div>
<div class="ascii-toggle-group"><span>CMD Colours</span><input type="checkbox" id="cmd-colours-toggle" class="ascii-checkbox" ${cmdColoursMode ? 'checked' : ''}></div>
<div class="ascii-toggle-group">
<span>Color Tint Mode</span>
<div class="ascii-color-picker-wrapper">
<input type="color" id="tint-picker" class="ascii-color-picker" value="${customTintHex}">
<input type="checkbox" id="tint-toggle" class="ascii-checkbox">
</div>
</div>
</details>
</div>
<div class="ascii-btn-row">
<button type="button" id="ascii-random-btn" class="ascii-action-btn">🎲 Random</button>
<button type="button" id="ascii-reset-btn" class="ascii-action-btn reset-variant">🔄 Reset</button>
</div>
`;
document.body.appendChild(panel);
panel.addEventListener('input', markRendererDirty, { passive: true });
panel.addEventListener('change', markRendererDirty, { passive: true });
// Events
document.getElementById('master-ascii-toggle').addEventListener('change', (e) => { asciiEnabled = e.target.checked; if (asciiEnabled) clearAndForceResizeBuffers(); });
document.getElementById('res-enable').addEventListener('change', (e) => { resolutionSettingEnabled = e.target.checked; syncGlobalVariablesToUi(); clearAndForceResizeBuffers(); });
document.getElementById('palette-enable').addEventListener('change', (e) => { paletteSettingEnabled = e.target.checked; syncGlobalVariablesToUi(); clearAndForceResizeBuffers(); });
document.getElementById('brightness-enable').addEventListener('change', (e) => { brightnessSettingEnabled = e.target.checked; syncGlobalVariablesToUi(); });
document.getElementById('contrast-enable').addEventListener('change', (e) => { contrastSettingEnabled = e.target.checked; syncGlobalVariablesToUi(); });
document.getElementById('sat-enable').addEventListener('change', (e) => { saturationSettingEnabled = e.target.checked; syncGlobalVariablesToUi(); });
document.getElementById('noise-enable').addEventListener('change', (e) => { noiseSettingEnabled = e.target.checked; syncGlobalVariablesToUi(); });
document.getElementById('blur-enable').addEventListener('change', (e) => { blurSettingEnabled = e.target.checked; syncGlobalVariablesToUi(); });
document.getElementById('res-slider').addEventListener('input', (e) => { charResolution = parseInt(e.target.value); document.getElementById('res-val').innerText = effectiveResolution(); });
document.getElementById('palette-slider').addEventListener('input', (e) => { paletteSizeSetting = parseInt(e.target.value); document.getElementById('palette-val').innerText = (paletteSettingEnabled ? paletteSizeSetting : DEFAULT_PALETTE_SIZE) + " Chars"; });
document.getElementById('brightness-slider').addEventListener('input', (e) => { brightnessSetting = parseFloat(e.target.value); document.getElementById('brightness-val').innerText = Math.round(effectiveBrightness() * 100) + "%"; });
document.getElementById('contrast-slider').addEventListener('input', (e) => { contrastSetting = parseFloat(e.target.value); document.getElementById('contrast-val').innerText = effectiveContrast().toFixed(1); });
document.getElementById('sat-slider').addEventListener('input', (e) => { saturationSetting = parseFloat(e.target.value); document.getElementById('sat-val').innerText = effectiveSaturation().toFixed(1); });
document.getElementById('noise-slider').addEventListener('input', (e) => { noiseSetting = parseInt(e.target.value); document.getElementById('noise-val').innerText = effectiveNoise(); });
document.getElementById('blur-slider').addEventListener('input', (e) => { blurSetting = parseFloat(e.target.value); document.getElementById('blur-val').innerText = effectiveBlur().toFixed(1) + "px"; });
document.getElementById('solid-toggle').addEventListener('change', (e) => { solidPixelMode = e.target.checked; });
document.getElementById('edge-toggle').addEventListener('change', (e) => { edgeDetectionMode = e.target.checked; });
document.getElementById('invert-toggle').addEventListener('change', (e) => { colorInversionMode = e.target.checked; });
document.getElementById('scanline-toggle').addEventListener('change', (e) => { scanlineMode = e.target.checked; });
document.getElementById('thermal-toggle').addEventListener('change', (e) => { thermalMode = e.target.checked; });
document.getElementById('cmd-colours-toggle').addEventListener('change', (e) => { cmdColoursMode = e.target.checked; });
document.getElementById('tint-toggle').addEventListener('change', (e) => { tintMode = e.target.checked; });
document.getElementById('tint-picker').addEventListener('input', (e) => { customTintHex = e.target.value; });
document.getElementById('ascii-random-btn').addEventListener('click', randomizeShaders);
document.getElementById('ascii-reset-btn').addEventListener('click', resetShaders);
}
const targetChatButtonWrapper = document.querySelector('[class*="styles_friendChatButton__"]');
if (targetChatButtonWrapper) {
let customButton = targetChatButtonWrapper.querySelector('.native-ascii-btn');
if (!customButton) {
customButton = document.createElement('button');
customButton.className = 'native-ascii-btn';
customButton.innerText = panelIsOpen ? '❌ CLOSE' : 'ASCII Shaders';
customButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
panelIsOpen = !panelIsOpen;
if (panel) {
if (panelIsOpen) {
panel.style.display = 'flex';
customButton.innerText = '❌ CLOSE';
} else {
panel.style.display = 'none';
customButton.innerText = 'ASCII Shaders';
}
}
});
targetChatButtonWrapper.appendChild(customButton);
}
}
}
function createAsciiOverlay(gameContainer) {
const existingWrapper = document.getElementById('ascii-art-canvas');
if (existingWrapper) {
asciiWrapper = existingWrapper;
displayCanvas = existingWrapper.querySelector('#ascii-display-canvas');
displayCtx = displayCanvas ? displayCanvas.getContext('2d') : null;
if (existingWrapper.parentElement !== gameContainer) {
gameContainer.appendChild(existingWrapper);
clearDisplayedAsciiFrame();
beginFirstFrameMode();
}
return;
}
asciiWrapper = document.createElement('div');
asciiWrapper.id = 'ascii-art-canvas';
displayCanvas = document.createElement('canvas');
displayCanvas.id = 'ascii-display-canvas';
displayCtx = displayCanvas.getContext('2d');
displayCanvas.width = Math.max(1, window.innerWidth);
displayCanvas.height = Math.max(1, window.innerHeight);
clearDisplayedAsciiFrame();
asciiWrapper.appendChild(displayCanvas);
gameContainer.appendChild(asciiWrapper);
// REMOVED createUiPanel() FROM HERE TO PREVENT FRAMERATE OVERRIDES
}
function coverPanoramaIfPossible() {
if (!asciiEnabled) return;
const liveGameContainer = document.querySelector('[data-qa=panorama]');
const gameContainer = liveGameContainer || (cachedGameContainer && cachedGameContainer.isConnected ? cachedGameContainer : null);
if (liveGameContainer && liveGameContainer !== cachedGameContainer) {
cachedGameContainer = liveGameContainer;
resetForPanoramaTransition();
}
if (gameContainer) {
createAsciiOverlay(gameContainer);
if (waitingForFirstAsciiFrame) beginFirstFrameMode();
}
}
function processPanoramaToAscii() {
if (!asciiEnabled) {
removeAsciiCanvasOnly();
return;
}
const liveGameContainer = document.querySelector('[data-qa=panorama]');
const gameContainer = liveGameContainer || (cachedGameContainer && cachedGameContainer.isConnected ? cachedGameContainer : null);
if (!gameContainer) return;
if (liveGameContainer && liveGameContainer !== cachedGameContainer) {
cachedGameContainer = liveGameContainer;
resetForPanoramaTransition();
}
createAsciiOverlay(gameContainer);
let webGlCanvas = (cachedWebGlCanvas && cachedWebGlCanvas.isConnected ? cachedWebGlCanvas : null) ||
gameContainer.querySelector('.widget-scene-canvas') ||
gameContainer.querySelector('canvas[data-is-intercepted-webgl="true"]');
if (!webGlCanvas) {
if (waitingForFirstAsciiFrame) currentFpsInterval = 1000 / FIRST_FRAME_FPS;
return;
}
if (transitionBlockedCanvas && webGlCanvas !== transitionBlockedCanvas) {
transitionBlockedCanvas = null;
transitionSameCanvasBlockUntil = 0;
}
if (
transitionBlockedCanvas &&
webGlCanvas === transitionBlockedCanvas &&
performance.now() < transitionSameCanvasBlockUntil
) {
forceRender = true;
currentFpsInterval = 1000 / FIRST_FRAME_FPS;
return;
}
if (transitionBlockedFrameSignature) {
const currentSignature = canvasFrameSignature(webGlCanvas);
if (
currentSignature &&
currentSignature === transitionBlockedFrameSignature &&
performance.now() < transitionBlockUntil
) {
forceRender = true;
currentFpsInterval = 1000 / FIRST_FRAME_FPS;
return;
}
transitionBlockedFrameSignature = "";
transitionBlockedCanvas = null;
transitionSameCanvasBlockUntil = 0;
transitionBlockUntil = 0;
}
if (webGlCanvas !== activeWebGlCanvas) {
activeWebGlCanvas = webGlCanvas;
beginFirstFrameMode();
clearDisplayedAsciiFrame();
clearAndForceResizeBuffers();
}
if (webGlCanvas.width <= 300 || webGlCanvas.height <= 150) {
if (waitingForFirstAsciiFrame) currentFpsInterval = 1000 / FIRST_FRAME_FPS;
return;
}
if (webGlCanvas.width !== lastKnownWebGLWidth || webGlCanvas.height !== lastKnownWebGLHeight) {
lastKnownWebGLWidth = webGlCanvas.width;
lastKnownWebGLHeight = webGlCanvas.height;
clearAndForceResizeBuffers();
if (waitingForFirstAsciiFrame) {
clearDisplayedAsciiFrame();
}
}
if (isMapInteractionActive()) {
currentFpsInterval = 1000 / IDLE_FPS;
return;
}
const now = performance.now();
const currentNoise = effectiveNoise();
const needsAnimatedFrame = currentNoise > 0;
let frameChanged = waitingForFirstAsciiFrame || forceRender || needsAnimatedFrame;
if (!waitingForFirstAsciiFrame && !frameChanged && now - lastChangeCheckTime >= 1000 / CHANGE_CHECK_FPS) {
lastChangeCheckTime = now;
frameChanged = frameSignatureChanged(webGlCanvas);
}
if (frameChanged) {
cameraMoving = true;
lastMovementTime = now;
} else if (now - lastMovementTime > 180 && !userInteracting) {
cameraMoving = false;
}
if (waitingForFirstAsciiFrame) {
currentFpsInterval = 1000 / FIRST_FRAME_FPS;
} else if (cameraMoving || userInteracting) {
currentFpsInterval = 1000 / MOVEMENT_FPS;
} else if (needsAnimatedFrame) {
currentFpsInterval = 1000 / ACTIVE_FPS;
} else {
currentFpsInterval = 1000 / IDLE_FPS;
}
if (!frameChanged && !forceRender && !needsAnimatedFrame) {
return;
}
const targetWidth = effectiveResolution();
const canvasAspect = webGlCanvas.height / webGlCanvas.width;
const targetHeight = Math.round(targetWidth * canvasAspect * 1.35);
if (
!targetWidth ||
!targetHeight ||
targetWidth < 2 ||
targetHeight < 2 ||
!isFinite(targetWidth) ||
!isFinite(targetHeight)
) {
return;
}
if (captureCanvas.width !== targetWidth || captureCanvas.height !== targetHeight) {
captureCanvas.width = targetWidth;
captureCanvas.height = targetHeight;
displayCanvas.width = targetWidth * 7;
displayCanvas.height = targetHeight * 10;
}
try {
captureCtx.clearRect(0, 0, targetWidth, targetHeight);
const currentBlur = effectiveBlur();
if (currentBlur > 0) { captureCtx.filter = `blur(${currentBlur}px)`; } else { captureCtx.filter = 'none'; }
captureCtx.drawImage(webGlCanvas, 0, 0, targetWidth, targetHeight);
captureCtx.filter = 'none';
const imgData = captureCtx.getImageData(0, 0, targetWidth, targetHeight);
const data = imgData.data;
if (!imageDataHasVisibleContent(data)) {
if (waitingForFirstAsciiFrame) {
forceRender = true;
currentFpsInterval = 1000 / FIRST_FRAME_FPS;
}
return;
}
forceRender = false;
displayCtx.fillStyle = '#05010a';
displayCtx.fillRect(0, 0, displayCanvas.width, displayCanvas.height);
displayCtx.font = "bold 11px 'Courier New', Courier, monospace";
displayCtx.textBaseline = "top";
const cellWidth = 7;
const cellHeight = 10;
const tintRgb = hexToRgb(customTintHex);
const currentPalette = getDynamicPalette();
const currentBrightness = effectiveBrightness();
const currentContrast = effectiveContrast();
const currentSaturation = effectiveSaturation();
for (let y = 0; y < targetHeight; y++) {
const isScanlineRow = scanlineMode && (y % 2 === 0);
for (let x = 0; x < targetWidth; x++) {
const i = (y * targetWidth + x) * 4;
let r = data[i]; let g = data[i+1]; let b = data[i+2];
if (currentBrightness !== 1.0) { r *= currentBrightness; g *= currentBrightness; b *= currentBrightness; }
if (colorInversionMode) { r = 255 - r; g = 255 - g; b = 255 - b; }
if (currentContrast !== 1.0) {
r = ((r - 128) * currentContrast) + 128;
g = ((g - 128) * currentContrast) + 128;
b = ((b - 128) * currentContrast) + 128;
}
if (currentSaturation !== 1.0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
r = gray + (r - gray) * currentSaturation; g = gray + (g - gray) * currentSaturation; b = gray + (b - gray) * currentSaturation;
}
if (currentNoise > 0) { const noise = generateGaussianNoise(currentNoise); r += noise; g += noise; b += noise; }
r = Math.min(255, Math.max(0, r)); g = Math.min(255, Math.max(0, g)); b = Math.min(255, Math.max(0, b));
let isEdge = false;
if (edgeDetectionMode && x > 0 && y > 0 && x < targetWidth - 1 && y < targetHeight - 1) {
const idxRight = i + 4; const idxDown = i + (targetWidth * 4);
const currentBrightness = 0.299 * r + 0.587 * g + 0.114 * b;
const rightBrightness = 0.299 * data[idxRight] + 0.587 * data[idxRight+1] + 0.114 * data[idxRight+2];
const downBrightness = 0.299 * data[idxDown] + 0.587 * data[idxDown+1] + 0.114 * data[idxDown+2];
if (Math.abs(currentBrightness - rightBrightness) > 30 || Math.abs(currentBrightness - downBrightness) > 30) isEdge = true;
}
const finalBrightness = 0.299 * r + 0.587 * g + 0.114 * b;
let charIndex = Math.floor((finalBrightness / 255) * (currentPalette.length - 1));
let char = currentPalette[(currentPalette.length - 1) - charIndex];
if (edgeDetectionMode) { char = isEdge ? '*' : ' '; }
else if (solidPixelMode) { char = char !== ' ' ? '█' : ' '; }
if (thermalMode && char !== ' ') {
const normalized = finalBrightness / 255;
if (normalized < 0.25) { r = 0; g = 0; b = 255 * (normalized / 0.25); }
else if (normalized < 0.5) { r = 255 * ((normalized - 0.25) / 0.25); g = 0; b = 255; }
else if (normalized < 0.75) { r = 255; g = 255 * ((normalized - 0.5) / 0.25); b = 0; }
else { r = 255; g = 255 * ((1.0 - normalized) / 0.25); b = 255 * ((normalized - 0.75) / 0.25); }
}
if (tintMode && !thermalMode && char !== ' ') {
const scalar = finalBrightness / 255;
r = tintRgb.r * scalar; g = tintRgb.g * scalar; b = tintRgb.b * scalar;
}
if (char !== ' ') {
if (isScanlineRow) { displayCtx.fillStyle = renderColourString(r * 0.25, g * 0.25, b * 0.25); }
else { displayCtx.fillStyle = renderColourString(r, g, b); }
displayCtx.fillText(char, x * cellWidth, y * cellHeight);
}
}
}
const scaleX = window.innerWidth / displayCanvas.width;
const scaleY = window.innerHeight / displayCanvas.height;
displayCanvas.style.transform = `scale(${scaleX * 1.005}, ${scaleY * 1.005})`;
waitingForFirstAsciiFrame = false;
} catch (e) {
if (waitingForFirstAsciiFrame) {
forceRender = true;
currentFpsInterval = 1000 / FIRST_FRAME_FPS;
}
}
}
function removeAsciiCanvasOnly() {
const existingCanvas = document.getElementById('ascii-art-canvas');
if (existingCanvas) existingCanvas.remove();
}
function removeFullUiOverlay() {
removeAsciiCanvasOnly();
const container = document.getElementById('ascii-control-panel');
if (container) container.remove();
}
function setupPanoramaMutationObserver(targetContainer) {
if (observedPanoramaContainer === targetContainer) return;
if (containerMutationObserver) containerMutationObserver.disconnect();
observedPanoramaContainer = targetContainer;
containerMutationObserver = new MutationObserver((mutations) => {
for (let mutation of mutations) {
if (mutation.type === 'childList') {
const ownOverlayChanged =
[...mutation.addedNodes].some(n => n.id === 'ascii-art-canvas') ||
[...mutation.removedNodes].some(n => n.id === 'ascii-art-canvas');
if (ownOverlayChanged) continue;
if (!mutationTouchesPanoramaCanvas(mutation)) continue;
resetForPanoramaTransition();
requestImmediateRender();
break;
}
}
});
containerMutationObserver.observe(targetContainer, { childList: true, subtree: true });
}
let lastFrameTime = 0;
function isGameRoute() {
return location.pathname.includes('/game/') ||
location.pathname.includes('/duels/') ||
location.pathname.includes('/challenge/') ||
location.pathname.includes('/battle-royale/') ||
location.pathname.includes('/multiplayer/') ||
location.pathname.includes('/singleplayer/');
}
function updateCachedElements() {
cachedIsGame = isGameRoute();
if (!cachedIsGame) {
cachedGameContainer = null;
cachedWebGlCanvas = null;
activeWebGlCanvas = null;
waitingForFirstAsciiFrame = true;
return;
}
const liveGameContainer = document.querySelector('[data-qa=panorama]');
if (liveGameContainer && liveGameContainer !== cachedGameContainer) {
if (cachedGameContainer) resetForPanoramaTransition();
cachedGameContainer = liveGameContainer;
} else {
cachedGameContainer = liveGameContainer;
}
cachedWebGlCanvas = cachedGameContainer
? cachedGameContainer.querySelector('.widget-scene-canvas') ||
cachedGameContainer.querySelector('canvas[data-is-intercepted-webgl="true"]')
: null;
}
function updateUiVisibility() {
if (!cachedGameContainer) return;
setupPanoramaMutationObserver(cachedGameContainer);
createUiPanel();
const panel = document.getElementById('ascii-control-panel');
const existingButton = document.querySelector('.native-ascii-btn');
const isResultsScreen = isResultsScreenActive();
const panoramaReady =
cachedWebGlCanvas &&
cachedWebGlCanvas.width > 300 &&
cachedWebGlCanvas.height > 150;
const shouldShowUi =
!!cachedGameContainer &&
!!existingButton &&
panoramaReady &&
!isResultsScreen;
if (existingButton) {
existingButton.style.display = shouldShowUi ? 'flex' : 'none';
}
if (panel) {
panel.style.display = panelIsOpen && shouldShowUi ? 'flex' : 'none';
}
}
function startLifecyclePolling() {
if (lifecycleCheckInterval) clearInterval(lifecycleCheckInterval);
lifecycleCheckInterval = setInterval(() => {
updateCachedElements();
if (!cachedIsGame) {
if (lastKnownWebGLWidth !== 0 || lastKnownWebGLHeight !== 0) {
lastKnownWebGLWidth = 0;
lastKnownWebGLHeight = 0;
clearAndForceResizeBuffers();
}
if (containerMutationObserver) {
containerMutationObserver.disconnect();
containerMutationObserver = null;
observedPanoramaContainer = null;
}
removeFullUiOverlay();
return;
}
updateUiVisibility();
if (cachedGameContainer && cachedWebGlCanvas && !document.getElementById('ascii-art-canvas')) {
forceRender = true;
requestImmediateRender();
}
}, 250);
}
function requestImmediateRender() {
forceRender = true;
if (renderQueued) return;
renderQueued = true;
requestAnimationFrame(() => {
renderQueued = false;
processPanoramaToAscii();
});
}
function liveRenderLoop(timestamp) {
requestAnimationFrame(liveRenderLoop);
if (document.hidden) return;
if (!asciiEnabled) {
removeAsciiCanvasOnly();
return;
}
if (!cachedIsGame || !cachedGameContainer) {
updateCachedElements();
}
if (!cachedIsGame || !cachedGameContainer) return;
coverPanoramaIfPossible();
const elapsed = timestamp - lastFrameTime;
if (elapsed < currentFpsInterval) return;
lastFrameTime = timestamp - (elapsed % currentFpsInterval);
processPanoramaToAscii();
}
window.addEventListener('mousemove', beginInteraction, { passive: true });
window.addEventListener('mousedown', beginInteraction, { passive: true });
window.addEventListener('pointermove', beginInteraction, { passive: true });
window.addEventListener('touchmove', beginInteraction, { passive: true });
window.addEventListener('wheel', beginInteraction, { passive: true });
window.addEventListener('wheel', handleMapWheel, { passive: true });
window.addEventListener('pointerdown', beginMapPointer, { passive: true });
window.addEventListener('pointerup', endMapPointer, { passive: true });
window.addEventListener('pointercancel', endMapPointer, { passive: true });
window.addEventListener('blur', cancelMapInteraction, { passive: true });
window.addEventListener('touchstart', beginMapPointer, { passive: true });
window.addEventListener('touchend', endMapPointer, { passive: true });
window.addEventListener('touchcancel', cancelMapInteraction, { passive: true });
window.addEventListener('pointerdown', handlePossibleRoundTransition, { capture: true, passive: true });
window.addEventListener('click', handlePossibleRoundTransition, { capture: true, passive: true });
window.addEventListener('keydown', handlePossibleRoundTransitionKey, { capture: true, passive: true });
document.addEventListener('visibilitychange', () => {
if (document.hidden) cancelMapInteraction();
}, { passive: true });
startLifecyclePolling();
updateCachedElements();
requestAnimationFrame(liveRenderLoop);