// ==UserScript==
// @name TESTE OMEP
// @namespace http://tampermonkey.net/
// @version 2.5
// @description Overlay da Missão para o site WPlace
// @author Víkish
// @match https://wplace.live/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=partidomissao.com
// @license MIT
// @grant none
// ==/UserScript==
(async function () {
'use strict';
/* =========================
CONFIG & DETECÇÕES DE RECURSOS
========================= */
const CHUNK_WIDTH = 1000;
const CHUNK_HEIGHT = 1000;
const UI_DEBOUNCE_MS = 200; // debounce para atualização da UI
const TOUCH_SWIPE_THRESHOLD = 12; // px para considerar swipe (global)
const TOUCH_TAP_MAX_MS = 400; // máximo de tempo para considerar tap
const SUPPORTS_OFFSCREEN = (typeof OffscreenCanvas !== 'undefined');
const SUPPORTS_CREATEIMAGEBITMAP = (typeof createImageBitmap === 'function');
const SUPPORTS_NOTIFICATION = (typeof Notification !== 'undefined');
let STORAGE_AVAILABLE = true;
try { localStorage && localStorage.getItem && localStorage.setItem; } catch (e) { STORAGE_AVAILABLE = false; }
/* =========================
WRAPPERS: safe canvas / localStorage / notification
========================= */
function createSafeCanvas(width, height) {
if (SUPPORTS_OFFSCREEN) {
try { return new OffscreenCanvas(width, height); } catch (e) { /* fallthrough to DOM canvas */ }
}
const c = document.createElement('canvas');
c.width = width;
c.height = height;
return c;
}
function safeLocalStorageGet(key) {
try {
return STORAGE_AVAILABLE ? localStorage.getItem(key) : null;
} catch (e) {
return null;
}
}
function safeLocalStorageSet(key, value) {
try {
if (STORAGE_AVAILABLE) localStorage.setItem(key, value);
} catch (e) { /* ignore */ }
}
/* in-page toast fallback for notifications (works on mobile & desktop) */
function showInPageNotification(title, body, icon, duration = 6000) {
try {
let container = document.getElementById('__overlay_toast_container');
if (!container) {
container = document.createElement('div');
container.id = '__overlay_toast_container';
Object.assign(container.style, {
position: 'fixed',
right: '12px',
top: '12px',
zIndex: 2147483647,
display: 'flex',
flexDirection: 'column',
gap: '8px',
pointerEvents: 'none',
maxWidth: '320px'
});
document.body.appendChild(container);
}
const box = document.createElement('div');
Object.assign(box.style, {
pointerEvents: 'auto',
display: 'flex',
gap: '8px',
alignItems: 'center',
background: 'rgba(0,0,0,0.85)',
color: '#fff',
padding: '10px 12px',
borderRadius: '10px',
boxShadow: '0 8px 28px rgba(0,0,0,0.45)',
transform: 'translateX(20px)',
opacity: '0',
transition: 'opacity 220ms ease, transform 220ms ease',
fontSize: '13px',
lineHeight: '1.2'
});
if (icon) {
const img = document.createElement('img');
img.src = icon;
Object.assign(img.style, { width:'36px', height:'36px', borderRadius:'6px', flex:'0 0 auto' });
box.appendChild(img);
}
const inner = document.createElement('div');
const t = document.createElement('div'); t.textContent = title; t.style.fontWeight = '700';
const b = document.createElement('div'); b.textContent = body; b.style.opacity = '0.95'; b.style.fontSize = '12px';
inner.appendChild(t); inner.appendChild(b);
box.appendChild(inner);
container.appendChild(box);
// animate in
requestAnimationFrame(()=>{ box.style.opacity = '1'; box.style.transform = 'translateX(0)'; });
const hide = () => {
box.style.opacity = '0'; box.style.transform = 'translateX(20px)';
setTimeout(()=>{ try { box.remove(); } catch(e){} }, 260);
};
const timeout = setTimeout(hide, duration);
box.addEventListener('click', ()=>{ clearTimeout(timeout); hide(); });
} catch (e) {
// ignore
}
}
function safeNotify(title, opt) {
try {
// Always attempt to show native Notification if supported and permitted
if (SUPPORTS_NOTIFICATION) {
if (Notification.permission === 'granted') {
try { new Notification(title, opt); } catch (e) { /* ignore */ }
} else if (Notification.permission !== 'denied') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
try { new Notification(title, opt); } catch (e) { /* ignore */ }
} else {
// fallback UI toast
showInPageNotification(title, opt && opt.body ? opt.body : '', opt && opt.icon ? opt.icon : null);
}
}).catch(()=>{ showInPageNotification(title, opt && opt.body ? opt.body : '', opt && opt.icon ? opt.icon : null); });
} else {
// permission denied
showInPageNotification(title, opt && opt.body ? opt.body : '', opt && opt.icon ? opt.icon : null);
}
} else {
// notifications not supported: use in-page toast
showInPageNotification(title, opt && opt.body ? opt.body : '', opt && opt.icon ? opt.icon : null);
}
} catch (e) {
// as ultimate fallback show console + toast
console.log('Notification fallback:', title, opt);
showInPageNotification(title, opt && opt.body ? opt.body : '', opt && opt.icon ? opt.icon : null);
}
}
/* =========================
HELPERS ASYNC: carregar overlays iniciais
========================= */
async function fetchData() {
const response = await fetch("https://gist.githubusercontent.com/yl99a/45ec3df57cc75c4b93c45251b87eb20b/raw/overlays.json?" + Date.now());
return await response.json();
}
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve({ img, width: img.naturalWidth, height: img.naturalHeight });
img.onerror = reject;
img.src = src;
});
}
function blobToImage(blob) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src = URL.createObjectURL(blob);
});
}
/* =========================
WORKER / PROCESSAMENTO: cria worker dinamicamente (processamento pesado)
Quando OffscreenCanvas não existe, cairemos para o processamento na thread principal.
========================= */
let pixelWorker = null;
let workerReqId = 0;
const pendingWorker = new Map();
async function processInMainThread(blob, overlayBuffer, overlayWidth, overlayHeight, selectedArr) {
try {
const overlayArr = new Uint8ClampedArray(overlayBuffer);
const selectedSet = new Set(selectedArr || []);
let imgBitmap = null;
if (SUPPORTS_CREATEIMAGEBITMAP) {
try { imgBitmap = await createImageBitmap(blob); } catch (e) { imgBitmap = null; }
}
const width = imgBitmap ? imgBitmap.width : overlayWidth || CHUNK_WIDTH;
const height = imgBitmap ? imgBitmap.height : overlayHeight || CHUNK_HEIGHT;
const canvas = createSafeCanvas(width, height);
const ctx = canvas.getContext('2d');
if (imgBitmap) {
ctx.drawImage(imgBitmap, 0, 0, width, height);
} else {
const img = await blobToImage(blob);
ctx.drawImage(img, 0, 0, width, height);
}
const originalData = ctx.getImageData(0, 0, width, height);
const resultData = ctx.getImageData(0, 0, width, height);
const d1 = originalData.data;
const dr = resultData.data;
const d2 = overlayArr;
let wrongPixels = 0;
let totalTargetPixels = 0;
const localColorCount = Object.create(null);
for (let i = 0; i < d1.length; i += 4) {
const a2 = d2[i + 3];
const isTransparent = a2 === 0;
if (!isTransparent) totalTargetPixels++;
const samePixel = d1[i] === d2[i] &&
d1[i+1] === d2[i+1] &&
d1[i+2] === d2[i+2] &&
d1[i+3] === d2[i+3];
const key = d2[i] + ',' + d2[i+1] + ',' + d2[i+2];
if (!samePixel && !isTransparent) {
wrongPixels++;
localColorCount[key] = (localColorCount[key] || 0) + 1;
}
if (samePixel && !isTransparent) {
dr[i] = 0; dr[i+1] = 255; dr[i+2] = 0; dr[i+3] = 255;
} else if (!isTransparent) {
if (selectedSet.has(key)) {
dr[i] = d2[i]; dr[i+1] = d2[i+1]; dr[i+2] = d2[i+2]; dr[i+3] = d2[i+3];
} else {
dr[i] = 0; dr[i+1] = 255; dr[i+2] = 0; dr[i+3] = 255;
}
}
}
ctx.putImageData(resultData, 0, 0);
let outBlob;
if (canvas instanceof OffscreenCanvas && typeof canvas.convertToBlob === 'function') {
outBlob = await canvas.convertToBlob();
} else {
outBlob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
}
return { blob: outBlob, wrongPixels, totalTargetPixels, colorCounts: localColorCount };
} catch (err) {
return Promise.reject(err);
}
}
if (SUPPORTS_OFFSCREEN) {
const workerCode = `
self.onmessage = async function(e) {
const data = e.data;
if (!data || data.type !== 'process') return;
const id = data.id;
try {
const blob = data.blob;
const overlayBuf = data.overlayBuffer;
const overlayWidth = data.overlayWidth;
const overlayHeight = data.overlayHeight;
const selectedArr = data.selectedColors || [];
const selectedSet = new Set(selectedArr);
const imgBitmap = await createImageBitmap(blob);
const width = imgBitmap.width;
const height = imgBitmap.height;
const off = new OffscreenCanvas(width, height);
const ctx = off.getContext('2d');
ctx.drawImage(imgBitmap, 0, 0, width, height);
const originalData = ctx.getImageData(0, 0, width, height);
const resultData = ctx.getImageData(0, 0, width, height);
const d1 = originalData.data;
const dr = resultData.data;
const d2 = new Uint8ClampedArray(overlayBuf);
let wrongPixels = 0;
let totalTargetPixels = 0;
const localColorCount = Object.create(null);
for (let i = 0; i < d1.length; i += 4) {
const a2 = d2[i + 3];
const isTransparent = a2 === 0;
if (!isTransparent) totalTargetPixels++;
const samePixel = d1[i] === d2[i] &&
d1[i+1] === d2[i+1] &&
d1[i+2] === d2[i+2] &&
d1[i+3] === d2[i+3];
const key = d2[i] + ',' + d2[i+1] + ',' + d2[i+2];
if (!samePixel && !isTransparent) {
wrongPixels++;
localColorCount[key] = (localColorCount[key] || 0) + 1;
}
if (samePixel && !isTransparent) {
dr[i] = 0; dr[i+1] = 255; dr[i+2] = 0; dr[i+3] = 255;
} else if (!isTransparent) {
if (selectedSet.has(key)) {
dr[i] = d2[i]; dr[i+1] = d2[i+1]; dr[i+2] = d2[i+2]; dr[i+3] = d2[i+3];
} else {
dr[i] = 0; dr[i+1] = 255; dr[i+2] = 0; dr[i+3] = 255;
}
}
}
ctx.putImageData(resultData, 0, 0);
const outBlob = await off.convertToBlob();
self.postMessage({ id, blob: outBlob, wrongPixels, totalTargetPixels, colorCounts: localColorCount });
} catch (err) {
self.postMessage({ id, error: String(err) });
}
};
`;
try {
const workerBlob = new Blob([workerCode], { type: "application/javascript" });
const workerUrl = URL.createObjectURL(workerBlob);
pixelWorker = new Worker(workerUrl);
pixelWorker.onmessage = (ev) => {
const data = ev.data;
if (!data || typeof data.id === 'undefined') return;
const id = data.id;
const entry = pendingWorker.get(id);
if (!entry) return;
pendingWorker.delete(id);
if (data.error) return entry.reject(new Error(data.error));
entry.resolve(data);
};
} catch (e) {
pixelWorker = null;
console.warn("Worker creation failed, falling back to main-thread processing.", e);
}
}
/* =========================
Carregar overlays e extrair imageData (usa canvas compatível)
========================= */
const overlays = await (async () => {
const arr = await fetchData();
for (const obj of arr) {
obj.chunksString = `/${obj.chunk[0]}/${obj.chunk[1]}.png`;
const { img, width, height } = await loadImage(obj.url);
const overlayCanvas = createSafeCanvas(1000, 1000);
const overlayCtx = overlayCanvas.getContext("2d");
overlayCtx.drawImage(img, obj.coords[0], obj.coords[1], width, height);
const imageData = overlayCtx.getImageData(0, 0, 1000, 1000);
obj.imageData = imageData;
}
return arr;
})();
/* =========================
Variáveis UI e estado
========================= */
const OVERLAY_MODES = ["overlay", "original", "chunks"];
let overlayMode = OVERLAY_MODES[0];
const counterContainer = document.createElement("div");
counterContainer.id = "pixel-counter";
Object.assign(counterContainer.style, {
position: "fixed", top: "5px", left: "50%", transform: "translateX(-50%)",
zIndex: "10000", padding: "6px 10px", fontFamily: "Arial, sans-serif",
backgroundColor: "rgba(0,0,0,0.66)", color: "white", borderRadius: "6px",
pointerEvents: "none", backdropFilter: "blur(3px)", lineHeight: "1.25",
textAlign: "center", display: "flex", flexDirection: "column", gap: "2px"
});
document.body.appendChild(counterContainer);
const pixelCounter = document.createElement("div");
pixelCounter.textContent = "Pixeis restantes: 0";
pixelCounter.style.fontSize = "11px";
counterContainer.appendChild(pixelCounter);
const percentageCounter = document.createElement("div");
percentageCounter.textContent = "Progresso atual: 0,00%";
percentageCounter.style.fontSize = "11px";
counterContainer.appendChild(percentageCounter);
/* =========================
UI: painel de cores (compacto)
========================= */
const colorStatsContainer = document.createElement("div");
Object.assign(colorStatsContainer.style, {
position: "fixed", top: "170px", left: "10px",
backgroundColor: "rgba(8,8,10,0.55)", color: "white", fontSize: "11px",
padding: "0px", borderRadius: "8px", zIndex: "10000",
maxHeight: "600px", pointerEvents: "auto",
minWidth: "110px", maxWidth: "320px", boxSizing: "border-box",
backdropFilter: "blur(6px)", touchAction: "manipulation"
});
document.body.appendChild(colorStatsContainer);
const styleTag = document.createElement('style');
styleTag.textContent = `
.color-list-scroll { overflow-y: auto; padding-right: 28px; position: relative; -ms-overflow-style: none; scrollbar-width: none; }
.color-list-scroll::-webkit-scrollbar { width: 0; height: 0; }
.color-checkbox { transform: scale(0.85); margin-left: 8px; margin-right: 2px; }
.color-item { display:flex; align-items:center; gap:8px; padding:6px 8px; border-radius:6px; }
.color-item:hover { background: rgba(255,255,255,0.03); }
.color-square { width: 18px; height: 18px; border-radius:3px; box-shadow: inset 0 0 0 1px rgba(0,0,0,0.12); flex: 0 0 auto; }
.color-count { white-space: nowrap; pointer-events: auto; cursor: pointer; font-size: 11px; color: #fff; }
.color-header { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:8px 10px; }
.color-title { font-weight:600; font-size:13px; padding-right:6px; }
.color-divider { height:1px; background: rgba(255,255,255,0.06); margin:0 8px 6px 8px; }
.minimize-btn { width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; border-radius:4px; border: 1px solid rgba(255,255,255,0.8); background: rgba(255,255,255,0.02); cursor:pointer; flex:0 0 auto; padding:3px; box-sizing: border-box; color: white; font-weight:600; }
.color-header.minimized-centered { justify-content: center; padding: 6px 0; }
.color-stats-collapsed {
width: 30px !important; height: 30px !important; min-width: 30px !important; max-width: 30px !important;
padding: 0 !important; border-radius: 6px !important; overflow: hidden; display:flex; align-items:center; justify-content:center;
}
.color-stats-collapsed .color-header { padding: 0; justify-content: center; width:100%; height:100%; }
.color-stats-collapsed .color-title, .color-stats-collapsed .color-divider, .color-stats-collapsed .controls-box, .color-stats-collapsed .color-list-scroll { display:none !important; }
.btn-square-max { width:20px; height:20px; border-radius:4px; border:1.5px solid white !important; background:transparent !important; display:inline-flex; align-items:center; justify-content:center; padding:0; touch-action: none; }
.btn-square-min { width:28px; height:28px; border-radius:4px; border:1.5px solid white !important; background:transparent !important; display:inline-flex; align-items:center; justify-content:center; padding:0; font-size:16px; line-height:1; }
.custom-scroll-track { position:absolute; right:6px; width:16px; display:block; box-sizing:border-box; z-index:10001; pointer-events:auto; }
.custom-scroll-btn { position:absolute; left:0; right:0; height:18px; display:flex; align-items:center; justify-content:center; font-size:10px; user-select:none; cursor:pointer; background: rgba(255,255,255,0.02); border-radius:4px; }
.custom-scroll-btn.up { top:0; } .custom-scroll-btn.down { bottom:0; }
.custom-scroll-thumb { position:absolute; left:0; right:0; border-radius:8px; background: rgba(255,255,255,0.12); cursor:grab; min-height:20px; box-sizing:border-box; transition: background 120ms ease; }
.custom-scroll-thumb:active { cursor:grabbing; background: rgba(255,255,255,0.18); }
`;
document.head.appendChild(styleTag);
// HEADER e botões
const header = document.createElement('div');
header.className = 'color-header';
header.style.width = '100%';
header.style.boxSizing = 'border-box';
const title = document.createElement('div'); title.className = 'color-title'; title.textContent = 'Tabela de Cores'; header.appendChild(title);
const minimizeBtn = document.createElement('button');
minimizeBtn.className = 'minimize-btn btn-square-min';
minimizeBtn.title = 'Minimizar';
minimizeBtn.style.pointerEvents = 'auto';
minimizeBtn.innerHTML = '-';
header.appendChild(minimizeBtn);
colorStatsContainer.appendChild(header);
const divider = document.createElement('div'); divider.className = 'color-divider'; colorStatsContainer.appendChild(divider);
const controlsBox = document.createElement('div'); controlsBox.className = 'controls-box';
Object.assign(controlsBox.style, { padding: '6px 8px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' });
controlsBox.style.pointerEvents = 'auto'; colorStatsContainer.appendChild(controlsBox);
const showAllWrapper = document.createElement('label'); showAllWrapper.className = 'controls-label'; showAllWrapper.style.cursor = 'pointer';
const showAllCheckbox = document.createElement('input'); showAllCheckbox.type = 'checkbox'; showAllCheckbox.checked = true; showAllCheckbox.style.marginRight = '6px'; showAllCheckbox.className = 'color-checkbox';
const showAllText = document.createElement('span'); showAllText.textContent = 'Mostrar todos';
showAllWrapper.appendChild(showAllCheckbox); showAllWrapper.appendChild(showAllText); controlsBox.appendChild(showAllWrapper);
const colorListWrapper = document.createElement('div'); colorListWrapper.className = 'color-list-scroll';
colorListWrapper.style.padding = '6px 8px 10px 8px'; colorListWrapper.style.position = 'relative'; colorListWrapper.style.overflowY = 'auto'; colorListWrapper.style.pointerEvents = 'auto';
colorStatsContainer.appendChild(colorListWrapper);
const colorList = document.createElement('div'); colorList.style.display = 'block'; colorListWrapper.appendChild(colorList);
// custom scrollbar
const customTrack = document.createElement('div'); customTrack.className = 'custom-scroll-track'; customTrack.style.position = 'absolute'; customTrack.style.pointerEvents = 'auto';
const customUpBtn = document.createElement('div'); customUpBtn.className = 'custom-scroll-btn up'; customUpBtn.textContent = '▲';
const customDownBtn = document.createElement('div'); customDownBtn.className = 'custom-scroll-btn down'; customDownBtn.textContent = '▼';
const customThumb = document.createElement('div'); customThumb.className = 'custom-scroll-thumb';
customTrack.appendChild(customUpBtn); customTrack.appendChild(customThumb); customTrack.appendChild(customDownBtn);
colorStatsContainer.appendChild(customTrack);
// popup nome da cor
const namePopup = document.createElement("div");
Object.assign(namePopup.style, { position: "fixed", left: "0px", top: "0px", transform: "translate(-50%,-100%)", backgroundColor: "rgba(0,0,0,0.9)", color: "white", padding: "6px 8px", borderRadius: "6px", fontSize: "12px", whiteSpace: "nowrap", display: "none", zIndex: "10001", pointerEvents: "none" });
document.body.appendChild(namePopup);
/* =========================
Estado e controle UI
========================= */
const selectedColors = new Set();
let lastColorCounts = {};
let showAll = true;
let savedCollapsed = safeLocalStorageGet('overlay_color_list_collapsed');
let listVisible = savedCollapsed === null ? false : (savedCollapsed === '0');
let initialSelectionDone = false;
/* debounce / merge updates */
let pendingColorCounts = null;
let uiDebounceTimer = null;
function scheduleColorUIUpdate(newCounts, wrongPixels, totalTarget) {
pendingColorCounts = newCounts || {};
if (typeof wrongPixels === 'number') {
pixelCounter.textContent = `Pixeis restantes: ${wrongPixels}`;
const percentage = totalTarget === 0 ? "100,00" : (((totalTarget - wrongPixels) / totalTarget) * 100).toFixed(2).replace(".", ",");
percentageCounter.textContent = `Progresso atual: ${percentage}%`;
}
if (uiDebounceTimer) clearTimeout(uiDebounceTimer);
uiDebounceTimer = setTimeout(() => {
lastColorCounts = Object.fromEntries(Object.entries(pendingColorCounts || {}).filter(([,c])=>c>0));
if (showAll) {
for (const k of Object.keys(lastColorCounts)) selectedColors.add(k);
}
renderColorListImmediate();
uiDebounceTimer = null;
}, UI_DEBOUNCE_MS);
}
/* =========================
Detecção global touch start/move/end para diferenciar swipe/tap
========================= */
let globalTouch = { startX:0, startY:0, startTime:0, isSwipe:false };
document.addEventListener('touchstart', (e) => {
if (!e.touches || !e.touches[0]) return;
const t = e.touches[0];
globalTouch.startX = t.clientX; globalTouch.startY = t.clientY; globalTouch.startTime = Date.now(); globalTouch.isSwipe = false;
}, { passive: true });
document.addEventListener('touchmove', (e) => {
if (!e.touches || !e.touches[0]) return;
const t = e.touches[0];
if (Math.abs(t.clientX - globalTouch.startX) > TOUCH_SWIPE_THRESHOLD || Math.abs(t.clientY - globalTouch.startY) > TOUCH_SWIPE_THRESHOLD) {
globalTouch.isSwipe = true;
}
}, { passive: true });
document.addEventListener('touchend', (e) => {
setTimeout(()=>{ globalTouch.isSwipe = false; }, 50);
}, { passive: true });
/* =========================
Funções de render / interação
========================= */
function rgbToHex(r,g,b){ const toHex=(v)=>v.toString(16).padStart(2,'0'); return `#${toHex(r)}${toHex(g)}${toHex(b)}`; }
/* -------------------------
Mapa de nomes de cores (maior, vindo do script anterior)
chaves em lowercase
------------------------- */
const colorNameMap = {
"#000000":"Black","#3c3c3c":"Dark gray","#787878":"Gray","#aaaaaa":"Medium gray","#d2d2d2":"Light gray","#ffffff":"White",
"#600018":"Deep red","#a50e1e":"Dark red","#ed1c24":"Red","#fa8072":"Light red",
"#e45c1a":"Dark orange","#ff7f27":"Orange","#f6aa09":"Gold","#f9dd3b":"Yellow","#fffabc":"Light yellow",
"#9c8431":"Dark goldenrod","#c5ad31":"Goldenrod","#e8d45f":"Light goldenrod",
"#4a6b3a":"Dark olive","#5a944a":"Olive","#84c573":"Light olive",
"#0eb968":"Dark green","#13e67b":"Green","#87ff5e":"Light green",
"#0c816e":"Dark teal","#10aea6":"Teal","#13e1be":"Light teal",
"#0f799f":"Dark cyan","#60f7f2":"Cyan","#bbfaf2":"Light cyan",
"#28509e":"Dark blue","#4093e4":"Blue","#7dc7ff":"Light blue",
"#4d31b8":"Dark indigo","#6b50f6":"Indigo","#99b1fb":"Light indigo",
"#4a4284":"Dark slate blue","#7a71c4":"Slate blue","#b5aef1":"Light slate blue",
"#780c99":"Dark purple","#aa38b9":"Purple","#e09ff9":"Light purple",
"#cb007a":"Dark pink","#ec1f80":"Pink","#f38da9":"Light pink",
"#9b5249":"Dark peach","#d18078":"Peach","#fab6a4":"Light peach",
"#684634":"Dark brown","#95682a":"Brown","#dba463":"Light brown",
"#7b6352":"Dark tan","#9c846b":"Tan","#d6b594":"Light tan",
"#d18051":"Dark beige","#f8b277":"Beige","#ffc5a5":"Light beige",
"#6d643f":"Dark stone","#948c6b":"Stone","#cdc59e":"Light stone",
"#333941":"Dark slate","#6d758d":"Slate","#b3b9d1":"Light slate"
};
/* -------------------------
Attaches handlers to show popup names (pointer + touch)
------------------------- */
function attachNameHandlers(el, hex) {
el.addEventListener('pointerenter', (ev) => {
if (ev.pointerType === 'touch') return;
clearPopupTimers();
showColorName(hex, el.getBoundingClientRect());
});
el.addEventListener('pointerleave', (ev) => {
if (ev.pointerType === 'touch') return;
clearPopupTimers();
popupHideTimeout = setTimeout(()=>{ hideColorName(); }, 120);
});
el.addEventListener('click', (ev) => {
if (ev.pointerType === 'touch') return;
clearPopupTimers();
showColorName(hex, el.getBoundingClientRect(), 1200);
});
let localTouchStartTime = 0;
el.addEventListener('touchstart', (ev) => {
if (!ev.touches || !ev.touches[0]) return;
localTouchStartTime = Date.now();
}, { passive: true });
el.addEventListener('touchend', (ev) => {
const dur = Date.now() - localTouchStartTime;
if (globalTouch.isSwipe) return;
if (dur > TOUCH_TAP_MAX_MS) return;
clearPopupTimers();
showColorName(hex, el.getBoundingClientRect(), 1200);
}, { passive: true });
}
let popupShowTimeout = null, popupHideTimeout = null, popupPersistentTimeout = null;
function clearPopupTimers(){
if (popupShowTimeout){ clearTimeout(popupShowTimeout); popupShowTimeout = null; }
if (popupHideTimeout){ clearTimeout(popupHideTimeout); popupHideTimeout = null; }
if (popupPersistentTimeout){ clearTimeout(popupPersistentTimeout); popupPersistentTimeout = null; }
}
function showColorName(hex, rect, persistForMs = 0) {
clearPopupTimers();
namePopup.textContent = colorNameMap[hex] || hex;
const left = rect.left + rect.width/2;
let top = rect.top - 6;
if (top < 8) top = rect.top + rect.height + 6;
namePopup.style.left = `${left}px`; namePopup.style.top = `${top}px`; namePopup.style.display = 'block';
if (persistForMs > 0) popupPersistentTimeout = setTimeout(()=>{ hideColorName(); }, persistForMs);
}
function hideColorName(){ clearPopupTimers(); namePopup.style.display = 'none'; }
/* =========================
renderColorListImmediate
========================= */
function renderColorListImmediate() {
colorList.innerHTML = "";
const entries = Object.entries(lastColorCounts || {}).filter(([,cnt]) => cnt > 0).sort((a,b)=>b[1]-a[1]);
if (!initialSelectionDone && showAll) {
for (const [key] of entries) selectedColors.add(key);
initialSelectionDone = true;
}
for (const [key, count] of entries) {
const [r,g,b] = key.split(",").map(Number);
const hex = rgbToHex(r,g,b);
const item = document.createElement('div');
item.className = 'color-item'; item.style.pointerEvents = 'auto';
const colorSquare = document.createElement('div'); colorSquare.className = 'color-square';
colorSquare.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; colorSquare.title = colorNameMap[hex] || '';
const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'color-checkbox';
checkbox.checked = selectedColors.has(key);
checkbox.addEventListener('change', (e)=> {
if (e.target.checked) selectedColors.add(key); else selectedColors.delete(key);
const allKeys = Object.keys(lastColorCounts).filter(k=>lastColorCounts[k]>0);
showAll = allKeys.length > 0 && allKeys.every(k=>selectedColors.has(k));
showAllCheckbox.checked = !!showAll;
refreshChunks();
scheduleColorUIUpdate(lastColorCounts);
});
const label = document.createElement('span'); label.className = 'color-count'; label.textContent = `${count}`;
attachNameHandlers(colorSquare, hex); attachNameHandlers(label, hex);
item.appendChild(colorSquare); item.appendChild(checkbox); item.appendChild(label);
colorList.appendChild(item);
}
if (Object.keys(lastColorCounts).length === 0) {
const aviso = document.createElement("div"); aviso.textContent = "Sem cores detectadas ainda."; aviso.style.fontSize = "10px"; aviso.style.opacity = "0.85"; aviso.style.marginTop = "4px";
colorList.appendChild(aviso);
}
needsLayoutUpdate = true;
scheduleUIUpdate();
}
/* ================ fim PARTE 1/2 ================ */
/* =========================
CONTINUAÇÃO: bindings, fetch proxy que usa o worker, UI helpers e finalização
========================= */
/* =========================
Debounce/Layout scheduler
========================= */
let rafScheduled = false;
let needsRenderColors = false;
let needsLayoutUpdate = false;
function scheduleUIUpdate() {
if (rafScheduled) return;
rafScheduled = true;
requestAnimationFrame(() => {
rafScheduled = false;
if (needsLayoutUpdate) {
needsLayoutUpdate = false;
try { updateListMaxHeight(); updateCustomTrackSize(); updateCustomThumb(); } catch (e) {}
}
if (needsRenderColors) {
needsRenderColors = false;
try { renderColorListImmediate(); } catch (e) {}
}
});
}
/* =========================
resize/scroll helpers para o painel
========================= */
function parsePx(v){ return Math.round(parseFloat(v) || 0); }
function updateListMaxHeight(){
try {
let containerH = colorStatsContainer.clientHeight || Math.round(colorStatsContainer.getBoundingClientRect().height || 0);
const headerH = header.offsetHeight || Math.round(header.getBoundingClientRect().height || 0);
const dividerH = divider.offsetHeight || Math.round(divider.getBoundingClientRect().height || 0);
const controlsH = controlsBox.offsetHeight || Math.round(controlsBox.getBoundingClientRect().height || 0);
const extra = 8;
if (!containerH || containerH < 80) {
const vp = Math.round(window.innerHeight || document.documentElement.clientHeight || 600);
containerH = Math.max(280, Math.round(vp * 0.30));
}
let maxH = Math.max(0, containerH - headerH - dividerH - controlsH - extra);
const MIN_LIST_HEIGHT = 280;
if (maxH < MIN_LIST_HEIGHT) maxH = Math.max(MIN_LIST_HEIGHT, Math.round(window.innerHeight * 0.25));
colorListWrapper.style.maxHeight = `${maxH}px`;
colorListWrapper.style.display = listVisible ? 'block' : colorListWrapper.style.display || 'block';
} catch (e) {
colorListWrapper.style.maxHeight = '420px';
colorListWrapper.style.display = 'block';
}
}
function updateCustomTrackSize(){
try {
const cs = getComputedStyle(colorListWrapper);
const padTop = parsePx(cs.paddingTop);
const padBottom = parsePx(cs.paddingBottom);
const wrapperH = colorListWrapper.clientHeight || Math.round(colorListWrapper.getBoundingClientRect().height || 0);
const trackInnerH = Math.max(0, wrapperH - padTop - padBottom);
const wrapperRect = colorListWrapper.getBoundingClientRect();
const containerRect = colorStatsContainer.getBoundingClientRect();
const topRel = Math.round(wrapperRect.top - containerRect.top + padTop);
customTrack.style.top = `${topRel}px`;
customTrack.style.height = `${trackInnerH}px`;
const sh = colorListWrapper.scrollHeight;
const ch = colorListWrapper.clientHeight;
customTrack.style.display = (sh <= ch) ? 'none' : 'block';
} catch (e) {
customTrack.style.top = `6px`;
customTrack.style.height = `${Math.max(0, colorListWrapper.clientHeight - 12)}px`;
}
}
function updateCustomThumb(){
updateCustomTrackSize();
const trackRect = customTrack.getBoundingClientRect();
const ch = colorListWrapper.clientHeight;
const sh = colorListWrapper.scrollHeight;
const upH = customUpBtn.offsetHeight || 18;
const downH = customDownBtn.offsetHeight || 18;
const trackH = Math.max(0, Math.round(trackRect.height));
const available = Math.max(0, trackH - upH - downH);
if (sh <= ch || available <= 0) { customThumb.style.height = `${Math.max(0, available)}px`; customThumb.style.top = `${upH}px`; customThumb.style.display = sh <= ch ? 'none' : 'block'; return; }
customThumb.style.display = 'block';
const fractionVisible = ch / sh;
const thumbH = Math.max(20, Math.round(available * fractionVisible));
customThumb.style.height = `${thumbH}px`;
const maxScroll = Math.max(0, sh - ch);
const scrollTop = Math.max(0, Math.min(colorListWrapper.scrollTop, maxScroll));
const movable = Math.max(0, available - thumbH);
const frac = maxScroll > 0 ? (scrollTop / maxScroll) : 0;
const topPos = Math.round(frac * movable);
customThumb.style.top = `${upH + topPos}px`;
}
colorListWrapper.addEventListener('scroll', ()=>{ updateCustomThumb(); }, { passive: true });
customUpBtn.addEventListener('click', ()=>{ const delta = Math.round(colorListWrapper.clientHeight * 0.6) || 60; colorListWrapper.scrollBy({ top: -delta, behavior: 'smooth' }); });
customDownBtn.addEventListener('click', ()=>{ const delta = Math.round(colorListWrapper.clientHeight * 0.6) || 60; colorListWrapper.scrollBy({ top: delta, behavior: 'smooth' }); });
/* drag thumb */
let dragging = false, dragStartY = 0, startScrollTop = 0;
const getPointerClientY = (ev) => (typeof ev.clientY === 'number') ? ev.clientY : (ev.touches && ev.touches[0] && ev.touches[0].clientY) || 0;
const onPointerMoveWhileDragging = (ev) => {
if (!dragging) return;
ev.preventDefault && ev.preventDefault();
const trackRect = customTrack.getBoundingClientRect(); const upH = customUpBtn.offsetHeight || 18; const downH = customDownBtn.offsetHeight || 18;
const trackH = Math.max(0, Math.round(trackRect.height)); const available = Math.max(0, trackH - upH - downH);
const sh = colorListWrapper.scrollHeight; const ch = colorListWrapper.clientHeight; const thumbH = customThumb.offsetHeight;
const clientY = getPointerClientY(ev); const dy = clientY - dragStartY; const scrollable = Math.max(0, sh - ch);
const movable = Math.max(1, Math.max(0, available - thumbH)); const deltaScroll = Math.round((dy / movable) * scrollable);
colorListWrapper.scrollTop = Math.max(0, Math.min(startScrollTop + deltaScroll, scrollable));
};
const stopDragging = (ev) => {
if (!dragging) return; dragging = false;
try { document.removeEventListener('pointermove', onPointerMoveWhileDragging); document.removeEventListener('pointerup', stopDragging); document.removeEventListener('touchmove', onPointerMoveWhileDragging); document.removeEventListener('touchend', stopDragging); } catch (e) {}
};
customThumb.addEventListener('pointerdown', (ev) => { ev.preventDefault && ev.preventDefault(); dragging = true; dragStartY = ev.clientY || 0; startScrollTop = colorListWrapper.scrollTop; document.addEventListener('pointermove', onPointerMoveWhileDragging, { passive:false }); document.addEventListener('pointerup', stopDragging); });
customThumb.addEventListener('touchstart', (ev) => { ev.preventDefault && ev.preventDefault(); const t = ev.touches && ev.touches[0]; dragging = true; dragStartY = t ? t.clientY : 0; startScrollTop = colorListWrapper.scrollTop; document.addEventListener('touchmove', onPointerMoveWhileDragging, { passive:false }); document.addEventListener('touchend', stopDragging); });
/* Mutation observer para lista */
const listObserver = new MutationObserver(()=>{ needsLayoutUpdate = true; scheduleUIUpdate(); });
listObserver.observe(colorList, { childList: true, subtree: true });
function refreshChunks(){ /* placeholder */ }
/* =========================
Min width dinâmico: mínimo = 1/3 da largura atual
========================= */
function ensureMinWidth() {
try {
const rect = colorStatsContainer.getBoundingClientRect();
const curWidth = Math.max(rect.width || 220, 220);
const minW = Math.round(curWidth / 3);
colorStatsContainer.style.minWidth = `${minW}px`;
} catch (e) {
colorStatsContainer.style.minWidth = `${Math.round(220/3)}px`;
}
}
/* =========================
Visibilidade / minimizar-maximizar (mantém 30x30 quando minimizado)
========================= */
function applyVisibility() {
if (listVisible) {
colorStatsContainer.classList.remove('color-stats-collapsed');
colorStatsContainer.style.minWidth = colorStatsContainer.style.minWidth || '220px';
colorStatsContainer.style.maxWidth = '320px';
colorStatsContainer.style.maxHeight = '420px';
colorListWrapper.style.display = 'block'; controlsBox.style.display = 'flex'; divider.style.display = 'block'; title.style.display = 'block';
minimizeBtn.classList.remove('btn-square-max'); minimizeBtn.classList.add('btn-square-min'); minimizeBtn.innerHTML = '-'; minimizeBtn.title = 'Minimizar'; header.classList.remove('minimized-centered');
} else {
colorStatsContainer.classList.add('color-stats-collapsed');
colorListWrapper.style.display = 'none'; controlsBox.style.display = 'none'; divider.style.display = 'none'; title.style.display = 'none';
minimizeBtn.classList.remove('btn-square-min'); minimizeBtn.classList.add('btn-square-max'); minimizeBtn.innerHTML = ''; minimizeBtn.title = 'Maximizar'; header.classList.add('minimized-centered');
}
ensureMinWidth();
needsLayoutUpdate = true;
scheduleUIUpdate();
}
minimizeBtn.addEventListener('click', (e)=>{ e.stopPropagation && e.stopPropagation(); listVisible = !listVisible; safeLocalStorageSet('overlay_color_list_collapsed', listVisible ? '0' : '1'); applyVisibility(); });
showAllCheckbox.addEventListener('change', (e)=>{ setAllSelected(e.target.checked); });
function setAllSelected(flag) {
if (!flag) {
selectedColors.clear(); showAll = false; initialSelectionDone = true; showAllCheckbox.checked = false; refreshChunks(); scheduleColorUIUpdate(lastColorCounts);
return;
}
selectedColors.clear(); for (const key of Object.keys(lastColorCounts)) selectedColors.add(key); showAll = true; showAllCheckbox.checked = true; refreshChunks(); scheduleColorUIUpdate(lastColorCounts);
}
/* =========================
FETCH PROXY: intercepta requests de chunks e despacha para worker OU main-thread
Protegido por try/catch caso sobrescrever fetch não seja permitido.
========================= */
try {
const originalFetch = fetch;
fetch = new Proxy(originalFetch, {
apply: async (target, thisArg, argList) => {
const urlString = typeof argList[0] === "object" ? argList[0].url : argList[0];
let url;
try { url = new URL(urlString); } catch (e) { return target.apply(thisArg, argList); }
if (overlayMode === "overlay") {
if (url.hostname === "backend.wplace.live" && url.pathname.startsWith("/files/")) {
for (const obj of overlays) {
if (url.pathname.endsWith(obj.chunksString)) {
const originalResponse = await target.apply(thisArg, argList);
const originalBlob = await originalResponse.blob();
const overlayCopy = obj.imageData.data.slice();
const overlayBuffer = overlayCopy.buffer;
if (pixelWorker) {
const id = ++workerReqId;
const promise = new Promise((resolve, reject) => {
const timeout = setTimeout(()=>{ pendingWorker.delete(id); reject(new Error("Worker timed out")); }, 15000);
pendingWorker.set(id, {
resolve: (data)=>{ clearTimeout(timeout); resolve(data); },
reject: (err)=>{ clearTimeout(timeout); reject(err); }
});
});
try {
pixelWorker.postMessage({
type: 'process',
id,
blob: originalBlob,
overlayBuffer,
overlayWidth: obj.imageData.width || CHUNK_WIDTH,
overlayHeight: obj.imageData.height || CHUNK_HEIGHT,
selectedColors: Array.from(selectedColors)
}, [overlayBuffer]);
} catch (e) {
pixelWorker.postMessage({
type: 'process',
id,
blob: originalBlob,
overlayBuffer,
overlayWidth: obj.imageData.width || CHUNK_WIDTH,
overlayHeight: obj.imageData.height || CHUNK_HEIGHT,
selectedColors: Array.from(selectedColors)
});
}
let result;
try {
result = await promise;
} catch (err) {
console.error("Worker processing failed:", err);
return originalResponse;
}
scheduleColorUIUpdate(result.colorCounts || {}, result.wrongPixels, result.totalTargetPixels);
return new Response(result.blob, { headers: { "Content-Type": "image/png" } });
} else {
let result;
try {
result = await processInMainThread(originalBlob, overlayBuffer, obj.imageData.width || CHUNK_WIDTH, obj.imageData.height || CHUNK_HEIGHT, Array.from(selectedColors));
} catch (err) {
console.error("Main-thread processing failed:", err);
return originalResponse;
}
scheduleColorUIUpdate(result.colorCounts || {}, result.wrongPixels, result.totalTargetPixels);
return new Response(result.blob, { headers: { "Content-Type": "image/png" } });
}
}
}
}
} else if (overlayMode === "chunks") {
if (url.hostname === "backend.wplace.live" && url.pathname.startsWith("/files/")) {
const parts = url.pathname.split("/");
const [chunk1, chunk2] = [parts.at(-2), parts.at(-1).split(".")[0]];
const canvas = createSafeCanvas(CHUNK_WIDTH, CHUNK_HEIGHT);
const ctx = canvas.getContext("2d", { willReadFrequently: true });
ctx.strokeStyle = 'red'; ctx.lineWidth = 1; ctx.strokeRect(0, 0, CHUNK_WIDTH, CHUNK_HEIGHT);
ctx.font = '30px Arial'; ctx.fillStyle = 'red'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(chunk1 + ", " + chunk2, CHUNK_WIDTH / 2, CHUNK_HEIGHT / 2);
let mergedBlob;
if (canvas instanceof OffscreenCanvas && typeof canvas.convertToBlob === 'function') {
mergedBlob = await canvas.convertToBlob();
} else {
mergedBlob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
}
return new Response(mergedBlob, { headers: { "Content-Type": "image/png" } });
}
}
return target.apply(thisArg, argList);
}
});
} catch (e) {
console.warn("Não foi possível sobrescrever fetch; o script continuará, mas a funcionalidade de overlay pode ser limitada.", e);
}
/* =========================
restante: load helpers, event handlers e patch UI
========================= */
/* observer para patch UI (botão de trocar overlayMode) */
function patchUI() {
if (document.getElementById("overlay-blend-button")) return;
let blendButton = document.createElement("button");
blendButton.id = "overlay-blend-button";
blendButton.textContent = overlayMode.charAt(0).toUpperCase() + overlayMode.slice(1);
blendButton.style.backgroundColor = "#0e0e0e7f";
blendButton.style.color = "white";
blendButton.style.border = "solid"; blendButton.style.borderColor = "#1d1d1d7f";
blendButton.style.borderRadius = "4px"; blendButton.style.padding = "5px 10px"; blendButton.style.cursor = "pointer";
blendButton.style.backdropFilter = "blur(2px)";
blendButton.addEventListener("click", () => {
overlayMode = OVERLAY_MODES[(OVERLAY_MODES.indexOf(overlayMode) + 1) % OVERLAY_MODES.length];
blendButton.textContent = overlayMode.charAt(0).toUpperCase() + overlayMode.slice(1);
});
const buttonContainer = document.querySelector("div.gap-4:nth-child(1) > div:nth-child(2)");
if (buttonContainer) buttonContainer.appendChild(blendButton);
}
const observer = new MutationObserver(()=>{ patchUI(); });
try {
const rootCandidate = document.querySelector("div.gap-4:nth-child(1)");
observer.observe(rootCandidate || document.body, { childList: true, subtree: true });
} catch (e) { observer.observe(document.body, { childList: true, subtree: true }); }
patchUI();
/* =========================
HELPERS ADICIONAIS (atalhos)
========================= */
function isEditableElement(el) {
if (!el) return false;
const tag = el.tagName && el.tagName.toUpperCase();
if (tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable) return true;
if (tag === 'SELECT') return true;
return false;
}
function simulateKeyPress(keyChar) {
try {
const kd = new KeyboardEvent('keydown', { key: keyChar, code: `Key${keyChar.toUpperCase()}`, keyCode: keyChar.toUpperCase().charCodeAt(0), which: keyChar.toUpperCase().charCodeAt(0), bubbles: true, cancelable: true });
const ku = new KeyboardEvent('keyup', { key: keyChar, code: `Key${keyChar.toUpperCase()}`, keyCode: keyChar.toUpperCase().charCodeAt(0), which: keyChar.toUpperCase().charCodeAt(0), bubbles: true, cancelable: true });
document.dispatchEvent(kd);
setTimeout(()=>document.dispatchEvent(ku), 15);
} catch (e) {
try {
document.dispatchEvent(new KeyboardEvent('keydown', { key: keyChar, bubbles: true }));
setTimeout(()=>document.dispatchEvent(new KeyboardEvent('keyup', { key: keyChar, bubbles: true })), 15);
} catch (ee) {}
}
}
function isVisible(el) {
if (!el) return false;
if (!(el instanceof Element)) return false;
const rect = el.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) return false;
const style = getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity || '1') === 0) return false;
return true;
}
/* --------------------------
findPaletteButton
-------------------------- */
function findPaletteButton() {
try {
const nodes = Array.from(document.querySelectorAll('div.flex.items-center.gap-2'));
const found = nodes.find(el => {
if (!el) return false;
const txt = (el.textContent || '').trim();
return txt.length > 0 && txt.startsWith('Pintar');
});
if (!found) return null;
const btn = found.querySelector('button') || found.querySelector('div[role="button"]') || found;
return btn || found;
} catch (e) { return null; }
}
/* =========================
Bind dos novos atalhos:
Middle click -> conta-gotas (svg.size-4.5)
Enter -> toggle paleta (botão "Pintar") e **fecha** paleta quando ela já estiver aberta
========================= */
document.addEventListener('mousedown', function(e){
try {
if (e.button === 1) {
if (isEditableElement(document.activeElement)) return;
e.preventDefault && e.preventDefault();
e.stopPropagation && e.stopPropagation();
const svg = document.querySelector('svg.size-4\\.5');
const target = svg ? (svg.closest('button') || svg.closest('div')) : null;
if (target && isVisible(target)) {
target.click();
return;
}
const paintBtn = findPaletteButton();
if (paintBtn) {
paintBtn.click();
setTimeout(()=>{
try {
const svg2 = document.querySelector('svg.size-4\\.5');
const target2 = svg2 ? (svg2.closest('button') || svg2.closest('div')) : null;
if (target2 && isVisible(target2)) target2.click();
} catch(_) {}
}, 300);
}
}
} catch (err) {}
}, { passive: false, capture: true });
document.addEventListener('keydown', function(e){
try {
if (e.key === 'Enter' || e.keyCode === 13) {
if (isEditableElement(document.activeElement)) return;
const btnContainer = findPaletteButton();
if (!btnContainer) return;
// resolve actual button element
const btn = (btnContainer.tagName && btnContainer.tagName.toLowerCase() === 'button') ? btnContainer : (btnContainer.querySelector && (btnContainer.querySelector('button') || btnContainer.querySelector('div[role="button"]')) ) || btnContainer;
// detect palette open: presence of a primary action button or modal - use existing selector used previously
const openBtn = document.querySelector('button.btn.btn-lg.btn-square.sm\\:btn-xl.btn-primary');
const isOpen = openBtn && isVisible(openBtn);
e.preventDefault && e.preventDefault();
e.stopPropagation && e.stopPropagation();
try { btn.focus && btn.focus({preventScroll:true}); } catch(_) {}
if (isOpen) {
// If paint palette is open, try to close it:
// 1) send Escape to close modals/panels that listen to it
// 2) if still open after short delay, click the paint toggle button to close it
try {
const esc = new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', which: 27, keyCode: 27, bubbles: true, cancelable: true });
document.dispatchEvent(esc);
} catch(_) {}
setTimeout(()=>{
const stillOpen = document.querySelector('button.btn.btn-lg.btn-square.sm\\:btn-xl.btn-primary');
if (stillOpen && isVisible(stillOpen)) {
try { btn.click(); } catch(e){ try { stillOpen.click(); } catch(_) {} }
}
}, 120);
return;
}
// otherwise (closed) just open by clicking paint toggle
try { btn.click(); } catch(err) {}
}
} catch (err) {}
}, { passive: false, capture: true });
/* =========================
ADIÇÃO: Long-press (1s) para ativar BORRACHA e desmarcar pixel (mobile)
========================= */
const LONG_PRESS_MS = 1000;
const LONG_PRESS_MOVE_TOLERANCE = 12;
let _lpTimer = null;
let _lpStartX = 0;
let _lpStartY = 0;
let _lpTarget = null;
function getEraserButtonByPath() {
try {
const candidates = Array.from(document.querySelectorAll('button.btn.btn-lg.btn-square.sm\\:btn-xl.shadow-md'));
for (const b of candidates) {
const path = b.querySelector('svg.size-5 path');
const d = path && path.getAttribute && path.getAttribute('d');
if (typeof d === 'string' && d.startsWith('M690')) {
return b;
}
}
const svgs = Array.from(document.querySelectorAll('svg.size-5'));
for (const s of svgs) {
const p = s.querySelector('path');
const d = p && p.getAttribute && p.getAttribute('d');
if (typeof d === 'string' && d.startsWith('M690')) {
return s.closest('button') || s.closest('div') || null;
}
}
} catch (e) {}
return null;
}
function findCanvasAtPoint(x,y) {
try {
let el = document.elementFromPoint(x, y);
if (el) {
if (el.tagName && el.tagName.toLowerCase() === 'canvas') return el;
const c = el.closest && el.closest('canvas');
if (c && isVisible(c)) return c;
}
const canvases = Array.from(document.querySelectorAll('canvas')).filter(isVisible);
for (const cv of canvases) {
const r = cv.getBoundingClientRect();
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return cv;
}
if (canvases.length) {
canvases.sort((a,b)=>{
const ra = a.getBoundingClientRect(), rb = b.getBoundingClientRect();
return (rb.width*rb.height) - (ra.width*ra.height);
});
return canvases[0];
}
} catch (e) {}
return null;
}
async function dispatchPointerClickToCanvas(canvas, clientX, clientY) {
if (!canvas) return false;
const rect = canvas.getBoundingClientRect();
const offsetX = clientX - rect.left;
const offsetY = clientY - rect.top;
try {
const pd = new PointerEvent('pointerdown', { bubbles:true, cancelable:true, composed:true, pointerId:1, pointerType:'touch', clientX, clientY, offsetX, offsetY, pressure:0.5 });
canvas.dispatchEvent(pd);
} catch(e){}
try { canvas.dispatchEvent(new MouseEvent('mousedown', { bubbles:true, cancelable:true, clientX, clientY, button:0 })); } catch(e){}
try { await new Promise(r => setTimeout(r, 18)); } catch(e){}
try { canvas.dispatchEvent(new PointerEvent('pointerup', { bubbles:true, cancelable:true, composed:true, pointerId:1, pointerType:'touch', clientX, clientY, offsetX, offsetY, pressure:0 })); } catch(e){}
try { canvas.dispatchEvent(new MouseEvent('mouseup', { bubbles:true, cancelable:true, clientX, clientY, button:0 })); } catch(e){}
try { canvas.dispatchEvent(new MouseEvent('click', { bubbles:true, cancelable:true, clientX, clientY, button:0 })); } catch(e){}
return true;
}
function clearLongPressTimer() {
if (_lpTimer) { clearTimeout(_lpTimer); _lpTimer = null; }
}
document.addEventListener('touchstart', function(e){
try {
if (!e.touches || !e.touches[0]) return;
if (isEditableElement(document.activeElement)) return;
if (colorStatsContainer && colorStatsContainer.contains(e.target)) return;
const t = e.touches[0];
_lpStartX = t.clientX;
_lpStartY = t.clientY;
_lpTarget = e.target;
clearLongPressTimer();
_lpTimer = setTimeout(async () => {
if (globalTouch.isSwipe) { clearLongPressTimer(); return; }
if (colorStatsContainer && colorStatsContainer.contains(_lpTarget)) { clearLongPressTimer(); return; }
const eraserBtn = getEraserButtonByPath();
if (!eraserBtn) { clearLongPressTimer(); return; }
try { eraserBtn.click(); } catch(e){}
await new Promise(r => setTimeout(r, 80));
const canvas = findCanvasAtPoint(_lpStartX, _lpStartY);
if (canvas) {
try { await dispatchPointerClickToCanvas(canvas, _lpStartX, _lpStartY); } catch(e){}
} else {
try {
const el = document.elementFromPoint(_lpStartX, _lpStartY);
if (el && !colorStatsContainer.contains(el)) { try { el.click(); } catch(e){} }
} catch(e){}
}
await new Promise(r => setTimeout(r, 90));
try { eraserBtn.click(); } catch(e){}
clearLongPressTimer();
}, LONG_PRESS_MS);
} catch (err) {
clearLongPressTimer();
}
}, { passive: true });
document.addEventListener('touchmove', function(e){
try {
if (!e.touches || !e.touches[0]) { clearLongPressTimer(); return; }
const t = e.touches[0];
const dx = Math.abs(t.clientX - _lpStartX);
const dy = Math.abs(t.clientY - _lpStartY);
if (dx > LONG_PRESS_MOVE_TOLERANCE || dy > LONG_PRESS_MOVE_TOLERANCE) {
clearLongPressTimer();
}
} catch (err) {
clearLongPressTimer();
}
}, { passive: true });
document.addEventListener('touchend', function(e){
clearLongPressTimer();
}, { passive: true });
/* =========================
ADIÇÃO: lógica de NOTIFICAÇÃO (Onça) — com safeNotify + localStorage protection
========================= */
/* estado notificação */
let onca_lastValue = "";
let onca_lastZeroTime = 0;
let onca_notifiedFor = null;
const ONCA_LOCAL_KEY = 'onca_notify_ts';
const ONCA_NOTIFY_DEBOUNCE_MS = 5000; // evita repetição entre abas
/* detecta (0:00) no DOM - observa mudanças */
const oncaDOMObserver = new MutationObserver(() => {
const el = document.querySelector('span.w-7.text-xs');
if (el) {
const txt = (el.textContent || '').trim();
if (txt === "(0:00)") {
onca_lastZeroTime = Date.now();
}
}
});
try {
oncaDOMObserver.observe(document.body, { childList: true, subtree: true, characterData: true });
} catch (e) { /* ignore */ }
/* função de envio de notificação segura */
function onca_sendNotification() {
try {
const now = Date.now();
try {
const last = parseInt(safeLocalStorageGet(ONCA_LOCAL_KEY) || '0', 10) || 0;
if (now - last < ONCA_NOTIFY_DEBOUNCE_MS) return; // outra aba já notificou recentemente
safeLocalStorageSet(ONCA_LOCAL_KEY, String(now));
} catch (e) { /* ignore localStorage errors */ }
const title = "Hora da onça beber água!";
const opt = {
body: "Tinta totalmente recarregada! 🔥",
icon: "https://i.imgur.com/mz6eWey.png",
timestamp: Date.now()
};
// Use both native notification (if possible) and in-page toast as fallback
safeNotify(title, opt);
// Also always show in-page toast to guarantee visibility on mobile
showInPageNotification(title, opt.body, opt.icon, 7000);
} catch (e) {
// swallow
}
}
/* hook fillText para detectar X/Y no canvas alvo */
const _origFillTextOnca = CanvasRenderingContext2D.prototype.fillText;
CanvasRenderingContext2D.prototype.fillText = function (text, x, y, maxWidth) {
try {
const canvas = this.canvas;
if (!canvas) return _origFillTextOnca.apply(this, arguments);
const dims = `[${canvas.width}x${canvas.height}]`;
const txt = String(text).trim();
// Verifica canvas do X/Y
if (dims === "[134x39]" && /^\d+\/\d+$/.test(txt)) {
const [curr, max] = txt.split("/").map(n => parseInt(n,10));
const [lastCurr, lastMax] = onca_lastValue ? onca_lastValue.split("/").map(n => parseInt(n,10)) : [null, null];
// detectar transição para full
if (lastCurr !== null && lastCurr < lastMax && curr === max) {
const diff = Date.now() - onca_lastZeroTime;
if (diff >= 0 && diff <= 2000) {
// evitar notificar repetidamente para o mesmo valor
const keyFor = `${curr}/${max}@${canvas.width}x${canvas.height}`;
if (onca_notifiedFor !== keyFor) {
onca_notifiedFor = keyFor;
onca_sendNotification();
}
}
}
// reset notified flag se voltar a gastar tinta
if (curr < max) {
onca_notifiedFor = null;
}
onca_lastValue = txt;
}
} catch (e) {
// swallow
}
return _origFillTextOnca.apply(this, arguments);
};
/* =========================
restante final: patch UI, inicialização e fallback
========================= */
ensureMinWidth();
applyVisibility();
/* fim do script */
})();