Assign hotkeys to all colors on wplace.live; supports side mouse buttons
// ==UserScript==
// @name wplace.live — Hotkeys All Colors
// @version 1.0
// @description Assign hotkeys to all colors on wplace.live; supports side mouse buttons
// @author Anayy_n
// @match https://wplace.live/*
// @grant none
// @license MIT
// @namespace https://greasyfork.org/users/1505124
// ==/UserScript==
(function() {
'use strict';
const STORAGE_KEY = 'wplace.manualHotkeys';
const SWATCH_SELECTOR = 'button[id^="color-"][aria-label]';
const PANEL_Z = 2147483647;
let keyMap = {};
let swatches = [];
function loadMap() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch { return {}; }
}
function saveMap() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(keyMap)); } catch {} }
function normKey(e) {
if(['Shift','Control','Alt','Meta'].includes(e.key)) return '';
let mods=[];
if(e.ctrlKey) mods.push('ctrl');
if(e.altKey) mods.push('alt');
if(e.shiftKey) mods.push('shift');
let k=e.key.toLowerCase();
if(k===' ') k='space';
if(k==='escape') k='esc';
return [...mods.sort(),k].join('+');
}
function isTyping(el){
if(!el) return false;
if(el.closest('#wphk-panel')) return true; // typing in panel
const t = el.tagName?.toLowerCase();
return t==='input'||t==='textarea'||el.isContentEditable;
}
function clickColor(el){
if(!el) return;
el.dispatchEvent(new MouseEvent('click',{bubbles:true,cancelable:true}));
}
function scanSwatches(){
swatches = Array.from(document.querySelectorAll(SWATCH_SELECTOR))
.filter(el=>el && el.offsetParent && el.getBoundingClientRect().width>0);
}
function onKeyDown(e){
if(e.repeat) return;
if(isTyping(document.activeElement)) return;
const key = normKey(e);
if(!key) return;
const id = keyMap[key];
if(!id) return;
const el = document.getElementById(id);
clickColor(el);
e.preventDefault();
e.stopPropagation();
}
function onMouseDown(e){
if(isTyping(document.activeElement)) return;
if(e.button === 0 || e.button === 2) return; // ignore left/right
let btn = '';
if(e.button === 3) btn = 'mouse4';
if(e.button === 4) btn = 'mouse5';
if(!btn) return;
const id = keyMap[btn];
if(!id) return;
const el = document.getElementById(id);
clickColor(el);
e.preventDefault();
e.stopPropagation();
}
function panelCSS(){
const css=`#wphk-btn{position:fixed;right:12px;bottom:12px;z-index:${PANEL_Z};border:0;padding:10px 12px;border-radius:8px;background:rgba(0,0,0,.7);color:#fff;font:13px/1.2 sans-serif;cursor:pointer}
#wphk-panel{position:fixed;right:12px;bottom:56px;z-index:${PANEL_Z};width:min(480px,92vw);max-height:70vh;background:#111;color:#eee;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.45);font:13px/1.4 system-ui,sans-serif;display:none}
#wphk-panel .hdr{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid #333}
#wphk-panel .hdr h3{margin:0;font-size:14px;font-weight:600}
#wphk-panel .hdr .actions{display:flex;gap:8px}
#wphk-panel .hdr button{border:0;padding:6px 8px;border-radius:6px;background:#222;color:#eaeaea;cursor:pointer}
#wphk-panel .hdr button:hover{background:#2b2b2b}
#wphk-panel .body{padding:8px 12px;overflow:auto;max-height:calc(70vh - 48px)}
#wphk-list{width:100%;border-collapse:collapse}
#wphk-list th,#wphk-list td{padding:6px 6px;border-bottom:1px solid #222;vertical-align:middle}
#wphk-list th{position:sticky;top:0;background:#111;z-index:1}
.swatch{width:24px;height:24px;border-radius:6px;border:1px solid rgba(255,255,255,.2)}
.name{opacity:.85}
.keybox{width:130px;padding:6px 8px;border-radius:6px;border:1px solid #333;background:#1a1a1a;color:#fff}
.keybox:focus{outline:1px solid #555}
.mini{font-size:11px;opacity:.8}`;
const tag=document.createElement('style');
tag.textContent=css;
document.head.appendChild(tag);
}
function colorOf(el){
const st=getComputedStyle(el);
const inline=el.getAttribute('style');
if(inline && /background\s*:\s*[^;]+/.test(inline)){
const m=inline.match(/background\s*:\s*([^;]+)/i);
if(m) return m[1].trim();
}
return st.backgroundColor||'#000';
}
function buildPanel(){
const openBtn=document.createElement('button');
openBtn.id='wphk-btn';
openBtn.textContent='🎨 Hotkeys';
document.body.appendChild(openBtn);
const panel=document.createElement('div');
panel.id='wphk-panel';
panel.innerHTML=`<div class="hdr">
<h3>Color Hotkeys</h3>
<div class="actions">
<button id="wphk-clear">Clear</button>
<button id="wphk-close">Close</button>
</div>
</div>
<div class="body">
<table id="wphk-list">
<thead>
<tr>
<th>Color</th>
<th>Name</th>
<th>Shortcut</th>
<th class="mini">Test</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="mini" style="margin-top:6px;">
Tip: assign keys freely; clicks on locked colors will just fail safely.
</div>
</div>`;
document.body.appendChild(panel);
openBtn.addEventListener('click', ()=>{
scanSwatches();
renderRows();
panel.style.display = panel.style.display==='none'||!panel.style.display?'block':'none';
});
panel.querySelector('#wphk-close').addEventListener('click', ()=>panel.style.display='none');
panel.querySelector('#wphk-clear').addEventListener('click', ()=>{
keyMap={};
saveMap();
renderRows();
});
function assignKeyToColor(keyOrBtn, colorId, input) {
// Remove previous key or previous color mapping
for (const [k,v] of Object.entries({...keyMap})) {
if(k === keyOrBtn || v === colorId) delete keyMap[k];
}
keyMap[keyOrBtn] = colorId;
saveMap();
// Update current input
input.value = keyOrBtn;
// Live clear other inputs using the same key
const tbody = document.querySelector('#wphk-list tbody');
if(tbody) {
const otherInputs = Array.from(tbody.querySelectorAll('.keybox'))
.filter(inp => inp !== input && inp.value === keyOrBtn);
otherInputs.forEach(inp => inp.value = '');
}
}
function renderRows(){
const tbody = panel.querySelector('#wphk-list tbody');
tbody.innerHTML='';
const rev = new Map();
for(const [k,v] of Object.entries(keyMap)) rev.set(v,k);
for(const el of swatches){
const id = el.id;
const name = el.getAttribute('aria-label')||id;
const bg = colorOf(el);
const tr = document.createElement('tr');
tr.innerHTML = `<td><div class="swatch" style="background:${bg}"></div></td>
<td class="name">${name}</td>
<td><input class="keybox" type="text" value="${rev.get(id)||''}" placeholder="press a key or mouse button"></td>
<td><button class="mini" data-test="${id}">Click</button></td>`;
const input = tr.querySelector('.keybox');
input.addEventListener('keydown', e=>{
e.preventDefault(); e.stopPropagation();
const nk = normKey(e);
if(!nk) return;
assignKeyToColor(nk, id, input);
});
input.addEventListener('mousedown', e=>{
if(e.button === 0 || e.button === 2) return;
let btn = '';
if(e.button === 3) btn = 'mouse4';
if(e.button === 4) btn = 'mouse5';
if(!btn) return;
assignKeyToColor(btn, id, input);
e.preventDefault();
e.stopPropagation();
});
tr.querySelector('button[data-test]').addEventListener('click', ()=>clickColor(el));
tbody.appendChild(tr);
}
}
buildPanel.renderRows = renderRows;
}
function init(){
keyMap = loadMap();
panelCSS();
buildPanel();
scanSwatches();
window.addEventListener('keydown', onKeyDown,{capture:true});
window.addEventListener('mousedown', onMouseDown,{capture:true});
const mo = new MutationObserver(()=>{
const oldCount = swatches.length;
scanSwatches();
if(swatches.length!==oldCount) buildPanel.renderRows?.();
});
mo.observe(document.body,{childList:true,subtree:true});
}
if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init);
else init();
})();