тЬи Personalize suas setas com controle total sobre cores, gradientes, espessura, opacidade e tamanho da ponta. тЩЯя╕ПЁЯФе
// ==UserScript==
// @name ЁЯОпЁЯОи Chess.com Custom Arrows
// @namespace http://tampermonkey.net/
// @version 7.2
// @description тЬи Personalize suas setas com controle total sobre cores, gradientes, espessura, opacidade e tamanho da ponta. тЩЯя╕ПЁЯФе
// @author aerus15
// @match *://*.chess.com/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// ==========================================
// 1. ESTADO GLOBAL E SALVAMENTO (LOCALSTORAGE)
// ==========================================
const savedGrad = localStorage.getItem('vfx-arrow-use-grad');
const STATE = {
thickness: parseFloat(localStorage.getItem('vfx-arrow-thick')) || 15,
useGradient: savedGrad !== null ? savedGrad === 'true' : true,
color1: localStorage.getItem('vfx-arrow-c1') || '#ff0000',
color2: localStorage.getItem('vfx-arrow-c2') || '#ffff00',
color3: localStorage.getItem('vfx-arrow-c3') || '#00ff00',
solidColor: localStorage.getItem('vfx-arrow-solid') || '#ffaa00',
opacity: parseFloat(localStorage.getItem('vfx-arrow-opacity')) || 0.8,
headSize: parseFloat(localStorage.getItem('vfx-arrow-hsize')) || 1.0,
headWidth: parseFloat(localStorage.getItem('vfx-arrow-hwidth')) || 1.0,
headLength: parseFloat(localStorage.getItem('vfx-arrow-hlength')) || 1.0,
drawnShapes: [],
currentShape: null,
isDragging: false,
boardElement: null,
svgOverlay: null,
squareSize: 0,
isFlipped: false
};
// ==========================================
// 2. CSS PARA ANULAR SETAS NATIVAS E BLINDAR CONTAINER
// ==========================================
const cssOverride = `
/* Oculta APENAS caminhos e pol├нgonos das setas nativas (preserva os c├нrculos e quadrados de outras extens├╡es) */
wc-chess-board .arrows path,
wc-chess-board .arrows line,
wc-chess-board .arrows polyline,
wc-chess-board .arrows polygon,
[data-cy="arrows"] path,
[data-cy="arrows"] line,
[data-cy="arrows"] polyline,
[data-cy="arrows"] polygon,
svg.arrows path,
svg.arrows line,
svg.arrows polyline,
svg.arrows polygon {
display: none !important;
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
}
#vfx-custom-arrows-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
/* Z-index extremo para garantir que fique em cima de TODAS as pe├зas e marca├з├╡es do outro script */
z-index: 2147483650 !important;
}
#vfx-master-panel {
position: fixed;
right: 15px;
bottom: 55px; /* Movido ~1cm para cima */
background: rgba(15, 15, 20, 0.95);
border: 1px solid #00e5ff;
border-radius: 8px;
padding: 15px;
z-index: 2147483647;
font-family: system-ui, sans-serif;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5), 0 0 15px rgba(0, 229, 255, 0.2);
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
gap: 12px;
width: 320px;
max-height: 85vh;
overflow-y: auto;
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transform-origin: bottom right;
}
#vfx-master-panel::-webkit-scrollbar { width: 6px; }
#vfx-master-panel::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); }
#vfx-master-panel::-webkit-scrollbar-thumb { background: #00e5ff; border-radius: 3px; }
#vfx-master-panel.minimized {
transform: scale(0);
pointer-events: none;
}
#vfx-close-btn {
position: absolute;
top: 10px;
right: 12px;
color: #ff4444;
font-size: 16px;
font-weight: bold;
cursor: pointer;
user-select: none;
line-height: 1;
transition: 0.2s;
}
#vfx-close-btn:hover {
color: #ff0000;
transform: scale(1.2);
}
.vfx-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.vfx-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #ccc;
flex: 1;
}
.vfx-input {
background: #222;
border: 1px solid #444;
color: white;
border-radius: 4px;
padding: 4px;
outline: none;
}
input[type="color"].vfx-input {
height: 28px;
width: 40px;
padding: 0;
cursor: pointer;
}
input[type="range"].vfx-slider {
width: 110px;
accent-color: #00e5ff;
cursor: pointer;
margin-right: 8px;
}
.vfx-val-display {
width: 30px;
text-align: right;
font-size: 11px;
font-weight: bold;
color: #00e5ff;
font-family: monospace;
}
.vfx-toggle {
cursor: pointer;
background: #333;
border: 1px solid #555;
padding: 6px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
transition: 0.2s;
text-align: center;
width: 100%;
margin-top: 5px;
user-select: none;
}
.vfx-toggle.active {
background: #00e5ff;
color: #000;
border-color: #00e5ff;
}
.vfx-divider {
height: 1px;
background: rgba(255,255,255,0.1);
margin: 5px 0;
}
#vfx-floating-btn {
position: fixed;
right: 15px;
bottom: 55px; /* Movido ~1cm para cima */
width: 45px;
height: 45px;
background: #111;
border: 2px solid #00e5ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
cursor: pointer;
z-index: 2147483648;
box-shadow: 0 0 15px rgba(0,229,255,0.4);
user-select: none;
transition: all 0.2s ease;
}
#vfx-floating-btn:hover {
transform: scale(1.1);
background: #222;
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = cssOverride;
(document.head || document.documentElement).appendChild(styleEl);
// ==========================================
// 3. MATEM├БTICA DO TABULEIRO
// ==========================================
const FILES = ['a','b','c','d','e','f','g','h'];
function getBoardState() {
if (!STATE.boardElement) return;
const rect = STATE.boardElement.getBoundingClientRect();
STATE.squareSize = rect.width / 8;
STATE.isFlipped = STATE.boardElement.classList.contains('flipped');
}
function pointToSquare(x, y) {
if (!STATE.boardElement || STATE.squareSize === 0) return null;
let fileIdx = Math.floor(x / STATE.squareSize);
let rankIdx = Math.floor(y / STATE.squareSize);
if (fileIdx < 0) fileIdx = 0;
if (fileIdx > 7) fileIdx = 7;
if (rankIdx < 0) rankIdx = 0;
if (rankIdx > 7) rankIdx = 7;
if (STATE.isFlipped) {
fileIdx = 7 - fileIdx;
} else {
rankIdx = 7 - rankIdx;
}
return FILES[fileIdx] + (rankIdx + 1);
}
function squareToPoint(sq) {
if (!STATE.boardElement || STATE.squareSize === 0 || !sq) return {x: 0, y: 0};
const fileIdx = FILES.indexOf(sq[0]);
const rankIdx = parseInt(sq[1]) - 1;
let visualFile, visualRank;
if (STATE.isFlipped) {
visualFile = 7 - fileIdx;
visualRank = rankIdx;
} else {
visualFile = fileIdx;
visualRank = 7 - rankIdx;
}
return {
x: (visualFile + 0.5) * STATE.squareSize,
y: (visualRank + 0.5) * STATE.squareSize
};
}
// ==========================================
// 4. MOTOR DE DESENHO (SVG)
// ==========================================
function setupSVG() {
if (!STATE.boardElement) return;
let container = STATE.boardElement.querySelector('#vfx-custom-arrows-container');
if (!container) {
container = document.createElement('div');
container.id = 'vfx-custom-arrows-container';
STATE.boardElement.appendChild(container);
}
// SVG Container - A opacidade ├й aplicada DIRETAMENTE AQUI
container.innerHTML = `
<svg id="vfx-main-svg" width="100%" height="100%" style="overflow: visible; opacity: ${STATE.opacity};">
<defs>
<linearGradient id="vfx-triple-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop id="vfx-stop-1" offset="0%" stop-color="${STATE.color1}" />
<stop id="vfx-stop-2" offset="50%" stop-color="${STATE.color2}" />
<stop id="vfx-stop-3" offset="100%" stop-color="${STATE.color3}" />
</linearGradient>
</defs>
<g id="vfx-shapes-layer"></g>
</svg>
`;
STATE.svgOverlay = container.querySelector('#vfx-shapes-layer');
}
function drawAll() {
if (!STATE.svgOverlay) return;
getBoardState();
STATE.svgOverlay.innerHTML = '';
STATE.drawnShapes.forEach(shape => renderShape(shape));
}
function renderShape(shape) {
// Ignora cliques simples na mesma casa
if (shape.start === shape.end) return;
const p1 = squareToPoint(shape.start);
const p2 = squareToPoint(shape.end);
const strokeColor = STATE.useGradient ? 'url(#vfx-triple-grad)' : STATE.solidColor;
const T = parseFloat(STATE.thickness);
const file1 = FILES.indexOf(shape.start[0]);
const rank1 = parseInt(shape.start[1]);
const file2 = FILES.indexOf(shape.end[0]);
const rank2 = parseInt(shape.end[1]);
const dx = Math.abs(file2 - file1);
const dy = Math.abs(rank2 - rank1);
const isKnight = (dx === 1 && dy === 2) || (dx === 2 && dy === 1);
const baseHeadSize = Math.max(15, T * 2.5) * STATE.headSize;
const headLength = baseHeadSize * STATE.headLength;
const headWidth = baseHeadSize * STATE.headWidth;
let pathD = '';
let arrowAngle = 0;
let finalP2 = { ...p2 };
if (isKnight) {
let cornerSq = (dy > dx) ? (shape.start[0] + shape.end[1]) : (shape.end[0] + shape.start[1]);
const pc = squareToPoint(cornerSq);
arrowAngle = Math.atan2(p2.y - pc.y, p2.x - pc.x);
const retraction = headLength - Math.min(T/2, headLength * 0.2);
finalP2.x = p2.x - retraction * Math.cos(arrowAngle);
finalP2.y = p2.y - retraction * Math.sin(arrowAngle);
pathD = `M ${p1.x} ${p1.y} L ${pc.x} ${pc.y} L ${finalP2.x} ${finalP2.y}`;
} else {
arrowAngle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
const retraction = headLength - Math.min(T/2, headLength * 0.2);
finalP2.x = p2.x - retraction * Math.cos(arrowAngle);
finalP2.y = p2.y - retraction * Math.sin(arrowAngle);
pathD = `M ${p1.x} ${p1.y} L ${finalP2.x} ${finalP2.y}`;
}
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathD);
path.setAttribute('class', 'vfx-shaft');
// BLINDAGEM: Aplicando propriedades inline com !important para vencer o outro script
path.style.setProperty('fill', 'none', 'important');
path.style.setProperty('stroke', strokeColor, 'important');
path.style.setProperty('stroke-width', T + 'px', 'important');
path.style.setProperty('stroke-linecap', 'round', 'important');
path.style.setProperty('stroke-linejoin', 'round', 'important');
const tipX = p2.x;
const tipY = p2.y;
const baseX = p2.x - headLength * Math.cos(arrowAngle);
const baseY = p2.y - headLength * Math.sin(arrowAngle);
const leftX = baseX - (headWidth/2) * Math.sin(arrowAngle);
const leftY = baseY + (headWidth/2) * Math.cos(arrowAngle);
const rightX = baseX + (headWidth/2) * Math.sin(arrowAngle);
const rightY = baseY - (headWidth/2) * Math.cos(arrowAngle);
const head = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
head.setAttribute('points', `${tipX},${tipY} ${leftX},${leftY} ${rightX},${rightY}`);
head.setAttribute('class', 'vfx-head');
// BLINDAGEM PARA A CABE├ЗA DA SETA
head.style.setProperty('fill', strokeColor, 'important');
head.style.setProperty('stroke', strokeColor, 'important');
head.style.setProperty('stroke-width', (T * 0.1) + 'px', 'important');
head.style.setProperty('stroke-linejoin', 'round', 'important');
STATE.svgOverlay.appendChild(path);
STATE.svgOverlay.appendChild(head);
}
// ==========================================
// 5. EVENTOS (SEM PREVIEW EM TEMPO REAL)
// ==========================================
function attachBoardEvents() {
if (!STATE.boardElement) return;
STATE.boardElement.addEventListener('contextmenu', e => e.preventDefault(), { capture: true });
STATE.boardElement.addEventListener('pointerdown', (e) => {
if (e.button === 2) {
e.preventDefault();
getBoardState();
const rect = STATE.boardElement.getBoundingClientRect();
const startSq = pointToSquare(e.clientX - rect.left, e.clientY - rect.top);
if (startSq) {
STATE.isDragging = true;
STATE.currentShape = { start: startSq, end: startSq };
}
} else if (e.button === 0) {
if (STATE.drawnShapes.length > 0) {
STATE.drawnShapes = [];
drawAll();
}
}
}, { capture: true });
window.addEventListener('pointermove', (e) => {
if (!STATE.isDragging || !STATE.currentShape || !STATE.boardElement) return;
const rect = STATE.boardElement.getBoundingClientRect();
const currSq = pointToSquare(e.clientX - rect.left, e.clientY - rect.top);
if (currSq && currSq !== STATE.currentShape.end) {
STATE.currentShape.end = currSq;
}
}, { capture: true });
window.addEventListener('pointerup', (e) => {
if (e.button === 2 && STATE.isDragging) {
STATE.isDragging = false;
if (STATE.currentShape && STATE.currentShape.start !== STATE.currentShape.end) {
const existsIdx = STATE.drawnShapes.findIndex(
s => s.start === STATE.currentShape.start && s.end === STATE.currentShape.end
);
if (existsIdx > -1) {
STATE.drawnShapes.splice(existsIdx, 1);
} else {
STATE.drawnShapes.push(STATE.currentShape);
}
}
STATE.currentShape = null;
drawAll(); // S├У AGORA DESENHAMOS A SETA
}
}, { capture: true });
}
// ==========================================
// 6. UI MASTER COM SAVE E LOAD E BOT├ГO FECHAR
// ==========================================
function initUI() {
if (document.getElementById('vfx-master-panel')) return;
const container = document.createElement('div');
container.innerHTML = `
<div id="vfx-floating-btn">ЁЯев</div>
<div id="vfx-master-panel" class="minimized">
<div id="vfx-close-btn">тЬХ</div>
<div style="text-align: center; color: #00e5ff; font-weight: 800; font-size: 14px; margin-bottom: 5px;">VFX ARROWS 7.2</div>
<div class="vfx-divider"></div>
<div class="vfx-row">
<span class="vfx-label">Espessura</span>
<input type="range" id="vfx-thick-slider" class="vfx-slider" min="0.5" max="40" step="0.5" value="${STATE.thickness}">
<span id="vfx-thick-val" class="vfx-val-display">${STATE.thickness}</span>
</div>
<div class="vfx-row">
<span class="vfx-label">Opacidade</span>
<input type="range" id="vfx-op-slider" class="vfx-slider" min="0.1" max="1" step="0.05" value="${STATE.opacity}">
<span id="vfx-op-val" class="vfx-val-display">${STATE.opacity}</span>
</div>
<div class="vfx-divider"></div>
<div style="font-size: 10px; color: #00e5ff; font-weight: bold; margin-bottom: 5px; text-transform: uppercase;">Cabe├зa da Seta</div>
<div class="vfx-row">
<span class="vfx-label">Tamanho Global</span>
<input type="range" id="vfx-hsize-slider" class="vfx-slider" min="0.2" max="3" step="0.1" value="${STATE.headSize}">
<span id="vfx-hsize-val" class="vfx-val-display">${STATE.headSize}</span>
</div>
<div class="vfx-row">
<span class="vfx-label">Largura (Gorda)</span>
<input type="range" id="vfx-hwidth-slider" class="vfx-slider" min="0.2" max="3" step="0.1" value="${STATE.headWidth}">
<span id="vfx-hwidth-val" class="vfx-val-display">${STATE.headWidth}</span>
</div>
<div class="vfx-row">
<span class="vfx-label">Comprimento (Pontuda)</span>
<input type="range" id="vfx-hlength-slider" class="vfx-slider" min="0.2" max="3" step="0.1" value="${STATE.headLength}">
<span id="vfx-hlength-val" class="vfx-val-display">${STATE.headLength}</span>
</div>
<div class="vfx-divider"></div>
<div id="vfx-mode-btn" class="vfx-toggle ${STATE.useGradient ? 'active' : ''}">${STATE.useGradient ? 'MODO: GRADIENTE' : 'MODO: COR ├ЪNICA'}</div>
<div id="vfx-solid-config" class="vfx-row" style="display: ${STATE.useGradient ? 'none' : 'flex'}; margin-top: 10px;">
<span class="vfx-label">Cor Principal</span>
<input type="color" id="vfx-c-solid" class="vfx-input" value="${STATE.solidColor}">
</div>
<div id="vfx-grad-config" style="display: ${STATE.useGradient ? 'flex' : 'none'}; flex-direction: column; gap: 8px; margin-top: 10px;">
<div class="vfx-row">
<span class="vfx-label">Cor In├нcio</span>
<input type="color" id="vfx-c1" class="vfx-input" value="${STATE.color1}">
</div>
<div class="vfx-row">
<span class="vfx-label">Cor Meio</span>
<input type="color" id="vfx-c2" class="vfx-input" value="${STATE.color2}">
</div>
<div class="vfx-row">
<span class="vfx-label">Cor Fim</span>
<input type="color" id="vfx-c3" class="vfx-input" value="${STATE.color3}">
</div>
</div>
</div>
`;
document.body.appendChild(container);
const bindSlider = (id, stateKey, storeKey) => {
const slider = document.getElementById(id);
const valEl = document.getElementById(id.replace('-slider', '-val'));
slider.oninput = (e) => {
STATE[stateKey] = parseFloat(e.target.value);
valEl.innerText = e.target.value;
localStorage.setItem(storeKey, e.target.value);
if (stateKey === 'opacity') {
const svgMain = document.getElementById('vfx-main-svg');
if (svgMain) svgMain.style.opacity = STATE.opacity;
} else {
drawAll();
}
};
};
bindSlider('vfx-thick-slider', 'thickness', 'vfx-arrow-thick');
bindSlider('vfx-op-slider', 'opacity', 'vfx-arrow-opacity');
bindSlider('vfx-hsize-slider', 'headSize', 'vfx-arrow-hsize');
bindSlider('vfx-hwidth-slider', 'headWidth', 'vfx-arrow-hwidth');
bindSlider('vfx-hlength-slider', 'headLength', 'vfx-arrow-hlength');
const btn = document.getElementById('vfx-floating-btn');
const panel = document.getElementById('vfx-master-panel');
const modeBtn = document.getElementById('vfx-mode-btn');
const solidConf = document.getElementById('vfx-solid-config');
const gradConf = document.getElementById('vfx-grad-config');
const closeBtn = document.getElementById('vfx-close-btn');
let panelOpen = false;
btn.onclick = () => {
panelOpen = !panelOpen;
panel.classList.toggle('minimized', !panelOpen);
btn.style.transform = panelOpen ? 'rotate(90deg)' : 'rotate(0deg)';
};
closeBtn.onclick = () => {
panelOpen = false;
panel.classList.add('minimized');
btn.style.transform = 'rotate(0deg)';
};
modeBtn.onclick = () => {
STATE.useGradient = !STATE.useGradient;
modeBtn.classList.toggle('active', STATE.useGradient);
modeBtn.innerText = STATE.useGradient ? 'MODO: GRADIENTE' : 'MODO: COR ├ЪNICA';
solidConf.style.display = STATE.useGradient ? 'none' : 'flex';
gradConf.style.display = STATE.useGradient ? 'flex' : 'none';
localStorage.setItem('vfx-arrow-use-grad', STATE.useGradient);
drawAll();
};
const updateGradientDef = () => {
const s1 = document.getElementById('vfx-stop-1');
const s2 = document.getElementById('vfx-stop-2');
const s3 = document.getElementById('vfx-stop-3');
if (s1 && s2 && s3) {
s1.setAttribute('stop-color', STATE.color1);
s2.setAttribute('stop-color', STATE.color2);
s3.setAttribute('stop-color', STATE.color3);
}
};
document.getElementById('vfx-c-solid').oninput = (e) => {
STATE.solidColor = e.target.value;
localStorage.setItem('vfx-arrow-solid', STATE.solidColor);
drawAll();
};
['c1', 'c2', 'c3'].forEach((id, idx) => {
document.getElementById(`vfx-${id}`).oninput = (e) => {
STATE[`color${idx+1}`] = e.target.value;
localStorage.setItem(`vfx-arrow-${id}`, e.target.value);
updateGradientDef();
};
});
}
// ==========================================
// 7. INICIALIZA├З├ГO
// ==========================================
function initSystem() {
const board = document.querySelector('wc-chess-board') || document.querySelector('.cg-board');
if (board && !STATE.boardElement) {
STATE.boardElement = board;
setupSVG();
attachBoardEvents();
initUI();
const resizeObserver = new ResizeObserver(() => requestAnimationFrame(drawAll));
resizeObserver.observe(board);
const classObserver = new MutationObserver(() => drawAll());
classObserver.observe(board, { attributes: true, attributeFilter: ['class'] });
}
}
const bootInterval = setInterval(() => {
initSystem();
if (STATE.boardElement) clearInterval(bootInterval);
}, 500);
document.addEventListener('DOMContentLoaded', initSystem);
})();