Skip to content

Instantly share code, notes, and snippets.

@CapsAdmin
Last active June 27, 2025 14:07
Show Gist options
  • Save CapsAdmin/56951114e29d220ff7c410ff83095e2c to your computer and use it in GitHub Desktop.
Save CapsAdmin/56951114e29d220ff7c410ff83095e2c to your computer and use it in GitHub Desktop.
(function () {
try {
window.mutationMonitor.stop();
} catch (e) {}
const config = {
root: document.getElementsByClassName("gmr-display")[0] || document.body,
visualDuration: 2000,
audioDuration: 100,
enableAudio: true,
};
let observer;
const observerConfig = {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true,
characterData: true,
characterDataOldValue: true,
};
const start = () => observer.observe(config.root, observerConfig);
const stop = () => observer.disconnect();
{
let audioContext;
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
if (audioContext.state === "suspended") {
audioContext.resume();
}
} catch (e) {
console.warn("Web Audio API not supported");
}
function playSound(frequency) {
if (!audioContext || !config.enableAudio) return;
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = frequency;
oscillator.type = "sine";
const now = audioContext.currentTime;
const duration = config.audioDuration / 1000;
gainNode.gain.setValueAtTime(0.1, now);
gainNode.gain.exponentialRampToValueAtTime(0.001, now + duration);
oscillator.start(now);
oscillator.stop(now + duration);
}
}
function getBoundingClientRectCached(element) {
if (!element.visualMutationsCachedRect) {
element.visualMutationsCachedRect = element.getBoundingClientRect();
}
return element.visualMutationsCachedRect;
}
const stats = {
added: 0,
removed: 0,
attributes: 0,
textChanges: 0,
attributeScores: {},
};
function spawnRect(type, e, msg) {
const colors = {
add: "rgba(255, 0, 0, 0.5)",
attribute: "rgba(0, 0, 255, 0.5)",
remove: "rgba(255, 165, 0, 0.5)",
textChange: "rgba(0, 255, 0, 0.5)",
};
const targetElement = e.nodeType === Node.TEXT_NODE ? e.parentElement : e;
if (!targetElement || !targetElement.getBoundingClientRect) {
return;
}
const elementRect = getBoundingClientRectCached(targetElement);
const rect = document.createElement("div");
rect.className = "mutation-overlay";
rect.style.position = "fixed";
rect.style.transform = targetElement.style.transform;
rect.style.top = elementRect.top + "px";
rect.style.left = elementRect.left + "px";
rect.style.width = elementRect.width + "px";
rect.style.height = elementRect.height + "px";
rect.style.zIndex = "999999";
rect.style.pointerEvents = "none";
rect.style.backgroundColor = colors[type];
rect.style.border = "2px solid " + colors[type].replace("0.5", "1");
rect.style.transition = `opacity ${config.visualDuration}ms ease-out`;
rect.style.opacity = "1";
if (msg) {
rect.style.display = "flex";
rect.style.justifyContent = "center";
rect.style.alignItems = "center";
rect.style.flexDirection = "column";
rect.style.fontSize = "8px";
rect.style.color = "white";
rect.textContent = msg;
}
document.body.appendChild(rect);
requestAnimationFrame(() => {
rect.style.opacity = "0";
});
setTimeout(() => {
if (rect.parentNode) {
rect.parentNode.removeChild(rect);
}
}, config.visualDuration);
}
function feedback(type, element, msg) {
if (type == "remove") {
playSound(500);
return;
} else if (type == "add") {
playSound(600);
} else if (type == "attribute") {
playSound(700);
} else if (type == "textChange") {
playSound(800);
}
spawnRect(type, element, msg);
}
function handleMutation(mutation) {
switch (mutation.type) {
case "childList":
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.className === "mutation-overlay") {
return;
}
stats.added++;
feedback("add", node);
}
});
mutation.removedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.className === "mutation-overlay") {
return;
}
stats.removed++;
feedback("remove", node);
}
});
break;
case "attributes":
if (mutation.target.className === "mutation-overlay") {
return;
}
let newVal = mutation.target.getAttribute(mutation.attributeName);
stats.attributes++;
let key = mutation.attributeName;
if (key === "class") {
let oldKey = "-class " + mutation.oldValue;
let newKey = "+class " + newVal;
stats.attributeScores[oldKey] = stats.attributeScores[oldKey] || 0;
stats.attributeScores[oldKey]++;
stats.attributeScores[newKey] = stats.attributeScores[newKey] || 0;
stats.attributeScores[newKey]++;
} else {
stats.attributeScores[key] = stats.attributeScores[key] || 0;
stats.attributeScores[key]++;
}
feedback(
"attribute",
mutation.target,
mutation.attributeName + " = " + newVal
);
break;
case "characterData":
stats.textChanges++;
feedback("textChange", mutation.target);
break;
}
}
observer = new MutationObserver((mutations) => {
mutations.forEach(handleMutation);
});
start();
window.mutationMonitor = {
observer: observer,
config: config,
stats: stats,
stop: stop,
restart: start,
getTextChangeStats: () => {
return {
textChanges: stats.textChanges,
totalMutations:
stats.added + stats.removed + stats.attributes + stats.textChanges,
};
},
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment