您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Overlay da Missão para o site WPlace
当前为
// ==UserScript== // @name teste Overlay da Missão: Edição Pro // @namespace http://tampermonkey.net/ // @version 2.4 // @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 ========================= */ 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 /* ========================= 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.onload = () => resolve(img); img.onerror = reject; img.src = URL.createObjectURL(blob); }); } /* ========================= WEB WORKER: cria worker dinamicamente (processamento pesado) ========================= */ 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) }); } }; `; const workerBlob = new Blob([workerCode], { type: "application/javascript" }); const workerUrl = URL.createObjectURL(workerBlob); const pixelWorker = new Worker(workerUrl); let workerReqId = 0; const pendingWorker = new Map(); 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); }; /* ========================= Carregar overlays e extrair imageData uma vez ========================= */ 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 = new OffscreenCanvas(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; const savedCollapsed = localStorage.getItem('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)}`; } 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; localStorage.setItem('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 ========================= */ fetch = new Proxy(fetch, { 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; 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); } }); }); 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]); 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 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 = new OffscreenCanvas(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); const mergedBlob = await canvas.convertToBlob(); return new Response(mergedBlob, { headers: { "Content-Type": "image/png" } }); } } return target.apply(thisArg, argList); } }); /* ========================= 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(); /* colorNameMap de exemplo */ const colorNameMap = { "#000000":"Preto", "#3c3c3c":"Cinza-escuro", "#787878":"Cinza", "#aaaaaa":"Cinza-médio", "#d2d2d2":"Cinza-claro", "#ffffff":"Branco" }; /* ========================= 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") ========================= */ 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; const btn = (btnContainer.tagName && btnContainer.tagName.toLowerCase() === 'button') ? btnContainer : (btnContainer.querySelector && (btnContainer.querySelector('button') || btnContainer.querySelector('div[role=\"button\"]')) ) || btnContainer; 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(_) {} try { btn.click(); } catch(err) {} if (isOpen) { setTimeout(()=>{ try { btn.click(); } catch(e){} }, 220); } } } 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 }); /* ========================= ADICIONEI AQUI: lógica de NOTIFICAÇÃO (Onça) - observe DOM (span.w-7.text-xs) para (0:00) - hook fillText para canvas [134x39] X/Y - only notify when evidence of timer (0:00) up to 2s before - avoid duplicates across tabs via localStorage ========================= */ /* 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 (somente Notification API + prevenção de duplicatas entre abas) */ function onca_sendNotification() { try { // evitar notificações muito frequentes entre abas const now = Date.now(); try { const last = parseInt(localStorage.getItem(ONCA_LOCAL_KEY) || '0', 10) || 0; if (now - last < ONCA_NOTIFY_DEBOUNCE_MS) return; // outra aba já notificou recentemente localStorage.setItem(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() }; if (Notification.permission === "granted") { new Notification(title, opt); } else if (Notification.permission !== "denied") { Notification.requestPermission().then(permission => { if (permission === "granted") { new Notification(title, opt); } }).catch(()=>{}); } } 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 ========================= */ /* observer para patch UI já definido acima */ ensureMinWidth(); applyVisibility(); /* fim do script */ })();