Created
February 10, 2026 00:20
-
-
Save jojobyte/19bd62d9fee091351e8efcc9314f1929 to your computer and use it in GitHub Desktop.
Bookmarklet to invert the colors of a website with Controls. One click inverts, second click removes the inversion. Paste as the URL inside a bookmark.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| javascript:void function(){ | |
| let b='#fff',toggle=true,i=95,ii=0,ia=100,h=180,hi=0,ha=360; | |
| const d=document,ce=(n)=>d.createElement(n),q=d.querySelectorAll(".dplgngr"),s=ce("style"),c=ce("form"),t='Doppelgänger'; | |
| if(q?.length)return q.forEach(a=>a.remove()); | |
| const invertStyles = (b,i,h) => `html { background-color: ${b}; } | |
| html,img,video,iframe { filter: invert(${i}%) hue-rotate(${h}deg); }`; | |
| const styles = (b,i,h,t) => `${t ? invertStyles(b,i,h) : ''} | |
| .dplgngr fieldset { border: 0 solid transparent; margin-top: 1em; padding: 0; display: grid; grid-template-areas: "l l" "r n"; grid-template-columns: 2fr 1fr; column-gap: 1em; } | |
| .dplgngr label { grid-area: l; } | |
| .dplgngr input { padding: 0; grid-area: r; } | |
| .dplgngr input[type="number"] { padding: 2px 4px; grid-area: n; } | |
| .dplgngr.ctrls { background: #f9f9f9; color: #111; position: fixed; display: block; z-index: 100000; top: 0; right: 0; bottom: auto; width: 300px; height: auto; padding: 1em; } | |
| `; | |
| function createSignal(initialValue) { | |
| let _value = initialValue; | |
| let _last = _value; | |
| const subs = []; | |
| function pub() { | |
| for (let s of subs) { | |
| s && s(_value, _last); | |
| } | |
| } | |
| return { | |
| get value() { return _value; }, | |
| set value(v) { | |
| _last = _value; | |
| _value = v; | |
| pub(); | |
| }, | |
| on: s => { | |
| const i = subs.push(s)-1; | |
| return () => { subs[i] = 0; }; | |
| }, | |
| }; | |
| } | |
| function template(html, elementOnly = true) { | |
| let node; | |
| const create = () => { | |
| const t = ce('template'); | |
| t.innerHTML = html; | |
| return t.content[elementOnly ? 'firstElementChild' : 'firstChild']; | |
| }; | |
| return () => (node || (node = create())).cloneNode(true); | |
| } | |
| function setupFields(element, initSignalVal) { | |
| let fieldType, style = createSignal(initSignalVal); | |
| let inputs = Object.fromEntries([...element.querySelectorAll('input')].map(e => [e.type,e])); | |
| const setNumericValue = val => (val >= 0 ? val : 0); | |
| const render = (v, p) => { | |
| console.log('setupFields render', { fieldType, v, }); | |
| if (!fieldType && !inputs.checkbox) { | |
| inputs.number.value = v; | |
| inputs.range.value = v; | |
| } | |
| if (fieldType === 'number') { | |
| inputs.range.value = v; | |
| } | |
| if (fieldType === 'range') { | |
| inputs.number.value = v; | |
| } | |
| if (inputs.range?.name === 'hue') { | |
| h = v; | |
| } | |
| if (inputs.range?.name === 'invert') { | |
| i = v; | |
| } | |
| if (fieldType === 'checkbox') { | |
| toggle = inputs.checkbox.checked; | |
| } | |
| s.innerText=styles(b, i, h, toggle); | |
| fieldType = undefined; | |
| }; | |
| const listener = (event) => { | |
| fieldType = event.target.type; | |
| let v = fieldType === 'checkbox' ? event.target.checked : setNumericValue(event.target.value); | |
| console.log('setupFields listener', { fieldType, v, event }); | |
| style.value = v; | |
| }; | |
| const styleOff = style.on(render); | |
| const unsub = () => { | |
| styleOff(); | |
| inputs.checkbox?.removeEventListener('input', listener); | |
| inputs.number?.removeEventListener('input', listener); | |
| inputs.range?.removeEventListener('input', listener); | |
| console.log('setupFields unsub', { inputs }); | |
| }; | |
| inputs.checkbox?.addEventListener('input', listener); | |
| inputs.number?.addEventListener('input', listener); | |
| inputs.range?.addEventListener('input', listener); | |
| render(style.value); | |
| return [element, style, unsub]; | |
| } | |
| let $invert = template(`<fieldset> | |
| <label for="invertVal">Invert Percentage</label> | |
| <input type="range" name="invert" min="${ii}" max="${ia}" value="${i}" /> | |
| <input type="number" name="invert" id="invertVal" min="${ii}" max="${ia}" value="${i}" /> | |
| </fieldset>`)(), | |
| $hue = template(`<fieldset> | |
| <label for="hueVal">Hue Rotate Degrees</label> | |
| <input type="range" name="hue" min="${hi}" max="${ha}" value="${h}" /> | |
| <input type="number" name="hue" id="hueVal" min="${hi}" max="${ha}" value="${h}" /> | |
| </fieldset>`)(), | |
| $toggle = template(`<fieldset> | |
| <label> | |
| <input type="checkbox" name="toggle"${toggle ? ' checked' : ''} /> Toggle ${t} | |
| </label> | |
| </fieldset>`)(); | |
| setupFields($invert, i); | |
| setupFields($hue, h); | |
| setupFields($toggle, toggle); | |
| s.type="text/css"; | |
| s.className="dplgngr"; | |
| s.innerText=styles(b, i, h, toggle); | |
| d.body.appendChild(s); | |
| c.className="dplgngr ctrls"; | |
| c.innerHTML=`<strong>${t} Controls</strong>`; | |
| c.insertAdjacentElement?.('beforeend', $invert); | |
| c.insertAdjacentElement?.('beforeend', $hue); | |
| c.insertAdjacentElement?.('beforeend', $toggle); | |
| d.body.appendChild(c); | |
| }(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| javascript:void function(){ let b='#fff',toggle=true,i=95,ii=0,ia=100,h=180,hi=0,ha=360; const d=document,ce=(n)=>d.createElement(n),q=d.querySelectorAll(".dplgngr"),s=ce("style"),c=ce("form"),t='Doppelgänger'; if(q?.length)return q.forEach(a=>a.remove()); const invertStyles = (b,i,h) => %60html { background-color: ${b}; } html,img,video,iframe { filter: invert(${i}%) hue-rotate(${h}deg); }%60; const styles = (b,i,h,t) => %60${t ? invertStyles(b,i,h) : ''} .dplgngr fieldset { border: 0 solid transparent; margin-top: 1em; padding: 0; display: grid; grid-template-areas: "l l" "r n"; grid-template-columns: 2fr 1fr; column-gap: 1em; } .dplgngr label { grid-area: l; } .dplgngr input { padding: 0; grid-area: r; } .dplgngr input[type="number"] { padding: 2px 4px; grid-area: n; } .dplgngr.ctrls { background: #f9f9f9; color: #111; position: fixed; display: block; z-index: 100000; top: 0; right: 0; bottom: auto; width: 300px; height: auto; padding: 1em; } %60; function createSignal(initialValue) { let _value = initialValue; let _last = _value; const subs = []; function pub() { for (let s of subs) { s && s(_value, _last); } } return { get value() { return _value; }, set value(v) { _last = _value; _value = v; pub(); }, on: s => { const i = subs.push(s)-1; return () => { subs[i] = 0; }; }, }; } function template(html, elementOnly = true) { let node; const create = () => { const t = ce('template'); t.innerHTML = html; return t.content[elementOnly ? 'firstElementChild' : 'firstChild']; }; return () => (node || (node = create())).cloneNode(true); } function setupFields(element, initSignalVal) { let fieldType, style = createSignal(initSignalVal); let inputs = Object.fromEntries([...element.querySelectorAll('input')].map(e => [e.type,e])); const setNumericValue = val => (val >= 0 ? val : 0); const render = (v, p) => { console.log('setupFields render', { fieldType, v, }); if (!fieldType && !inputs.checkbox) { inputs.number.value = v; inputs.range.value = v; } if (fieldType === 'number') { inputs.range.value = v; } if (fieldType === 'range') { inputs.number.value = v; } if (inputs.range?.name === 'hue') { h = v; } if (inputs.range?.name === 'invert') { i = v; } if (fieldType === 'checkbox') { toggle = inputs.checkbox.checked; } s.innerText=styles(b, i, h, toggle); fieldType = undefined; }; const listener = (event) => { fieldType = event.target.type; let v = fieldType === 'checkbox' ? event.target.checked : setNumericValue(event.target.value); console.log('setupFields listener', { fieldType, v, event }); style.value = v; }; const styleOff = style.on(render); const unsub = () => { styleOff(); inputs.checkbox?.removeEventListener('input', listener); inputs.number?.removeEventListener('input', listener); inputs.range?.removeEventListener('input', listener); console.log('setupFields unsub', { inputs }); }; inputs.checkbox?.addEventListener('input', listener); inputs.number?.addEventListener('input', listener); inputs.range?.addEventListener('input', listener); render(style.value); return [element, style, unsub]; } let $invert = template(%60<fieldset> <label for="invertVal">Invert Percentage</label> <input type="range" name="invert" min="${ii}" max="${ia}" value="${i}" /> <input type="number" name="invert" id="invertVal" min="${ii}" max="${ia}" value="${i}" /> </fieldset>%60)(), $hue = template(%60<fieldset> <label for="hueVal">Hue Rotate Degrees</label> <input type="range" name="hue" min="${hi}" max="${ha}" value="${h}" /> <input type="number" name="hue" id="hueVal" min="${hi}" max="${ha}" value="${h}" /> </fieldset>%60)(), $toggle = template(%60<fieldset> <label> <input type="checkbox" name="toggle"${toggle ? ' checked' : ''} /> Toggle ${t} </label> </fieldset>%60)(); setupFields($invert, i); setupFields($hue, h); setupFields($toggle, toggle); s.type="text/css"; s.className="dplgngr"; s.innerText=styles(b, i, h, toggle); d.body.appendChild(s); c.className="dplgngr ctrls"; c.innerHTML=%60<strong>${t} Controls</strong>%60; c.insertAdjacentElement?.('beforeend', $invert); c.insertAdjacentElement?.('beforeend', $hue); c.insertAdjacentElement?.('beforeend', $toggle); d.body.appendChild(c); }(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment