Created
September 8, 2026 09:38
-
-
Save marijnvdwerf/771ce3f867a943247bc952a17c3d2613 to your computer and use it in GitHub Desktop.
decomp.dev treemap userscript
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
| // ==UserScript== | |
| // @name decomp.dev treemap theme | |
| // @namespace https://decomp.dev/ | |
| // @description Recolors the interactive progress treemap with light/dark palettes. | |
| // @match https://decomp.dev/* | |
| // @match http://localhost:3000/* | |
| // @run-at document-start | |
| // @inject-into page | |
| // @grant none | |
| // ==/UserScript== | |
| (() => { | |
| // Percentage of black mixed into the outer stop of each unit's radial | |
| // gradient. 0 disables the gradient (flat fills). | |
| const OUTER_DARKEN = 0; | |
| // 0-99.9% ramp: 'gray' (perceptually matched to the green) or 'blue'. | |
| const PROGRESS_SCHEME = 'blue'; | |
| const PROGRESS_END = { | |
| gray: { dark: '#858c9c', light: '#898c94' }, | |
| blue: { dark: '#0094e8', light: '#3994d1' }, | |
| }; | |
| const PALETTES = { | |
| dark: { | |
| background: '#181c25', | |
| border: '#000', | |
| hoverBorder: '#fff', | |
| progressStart: '#1c212c', | |
| progressEnd: PROGRESS_END[PROGRESS_SCHEME].dark, | |
| complete: '#00a66e', | |
| filteredAlpha: 0.1, | |
| }, | |
| light: { | |
| background: '#fff', | |
| border: '#fff', | |
| hoverBorder: '#000', | |
| progressStart: '#e7eaf0', | |
| progressEnd: PROGRESS_END[PROGRESS_SCHEME].light, | |
| complete: '#00a66e', | |
| filteredAlpha: 0.1, | |
| }, | |
| }; | |
| const darken = (color) => | |
| OUTER_DARKEN | |
| ? `color-mix(in srgb, ${color}, #000 ${OUTER_DARKEN}%)` | |
| : color; | |
| const palette = () => { | |
| const attr = document.documentElement.getAttribute('data-theme'); | |
| if (attr === 'light' || attr === 'dark') { | |
| return PALETTES[attr]; | |
| } | |
| const dark = | |
| window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? true; | |
| return dark ? PALETTES.dark : PALETTES.light; | |
| }; | |
| // Literals painted by js/treemap.ts on the site. | |
| const SITE_BACKGROUND = '#181c25'; | |
| const SITE_BORDER = '#000'; | |
| const SITE_HOVER_BORDER = '#fff'; | |
| const SITE_COMPLETE_INNER = 'hsl(120 100% 39%)'; | |
| const SITE_COMPLETE_OUTER = 'hsl(120 100% 17%)'; | |
| const SITE_PROGRESS_INNER = | |
| /^color-mix\(in srgb, hsl\(200 0% 21%\), hsl\(200 100% 35%\) ([\d.]+)%\)$/; | |
| const SITE_PROGRESS_OUTER = | |
| /^color-mix\(in srgb, hsl\(200 0% 15%\), hsl\(200 100% 15%\) ([\d.]+)%\)$/; | |
| const SITE_FILTERED_ALPHA = 0.1; | |
| const remapStop = (color) => { | |
| const p = palette(); | |
| if (color === SITE_COMPLETE_INNER) return p.complete; | |
| if (color === SITE_COMPLETE_OUTER) return darken(p.complete); | |
| let m = SITE_PROGRESS_INNER.exec(color); | |
| if (m) { | |
| return `color-mix(in srgb, ${p.progressStart}, ${p.progressEnd} ${m[1]}%)`; | |
| } | |
| m = SITE_PROGRESS_OUTER.exec(color); | |
| if (m) { | |
| return darken( | |
| `color-mix(in srgb, ${p.progressStart}, ${p.progressEnd} ${m[1]}%)`, | |
| ); | |
| } | |
| return color; | |
| }; | |
| const patchSetter = (proto, prop, remap) => { | |
| const desc = Object.getOwnPropertyDescriptor(proto, prop); | |
| Object.defineProperty(proto, prop, { | |
| ...desc, | |
| set(value) { | |
| desc.set.call(this, remap(value)); | |
| }, | |
| }); | |
| }; | |
| const ctxProto = CanvasRenderingContext2D.prototype; | |
| // The site clears the canvas with fillRect; the tooltip paints with fill(). | |
| // Only remap the former: in Pico's light theme the tooltip background is | |
| // also #181c25, and remapping it would make the tooltip white-on-white. | |
| const fillRect = ctxProto.fillRect; | |
| ctxProto.fillRect = function (...args) { | |
| if (this.fillStyle !== SITE_BACKGROUND) { | |
| return fillRect.apply(this, args); | |
| } | |
| this.fillStyle = palette().background; | |
| try { | |
| return fillRect.apply(this, args); | |
| } finally { | |
| this.fillStyle = SITE_BACKGROUND; | |
| } | |
| }; | |
| patchSetter(ctxProto, 'strokeStyle', (v) => { | |
| if (v === SITE_BORDER) return palette().border; | |
| if (v === SITE_HOVER_BORDER) return palette().hoverBorder; | |
| return v; | |
| }); | |
| patchSetter(ctxProto, 'globalAlpha', (v) => | |
| v === SITE_FILTERED_ALPHA ? palette().filteredAlpha : v, | |
| ); | |
| const addColorStop = CanvasGradient.prototype.addColorStop; | |
| CanvasGradient.prototype.addColorStop = function (offset, color) { | |
| return addColorStop.call(this, offset, remapStop(color)); | |
| }; | |
| // The site's unit cache is private; make it rebuild on theme changes by | |
| // re-running its filter handler, or nudging the canvas size as a fallback. | |
| const repaint = () => { | |
| const filter = document.querySelector('input[name="filter"]'); | |
| if (filter instanceof HTMLInputElement) { | |
| filter.dispatchEvent(new Event('input', { bubbles: true })); | |
| return; | |
| } | |
| const canvas = document.getElementById('treemap'); | |
| if (!(canvas instanceof HTMLCanvasElement)) return; | |
| const height = canvas.style.height; | |
| canvas.style.height = `${canvas.getBoundingClientRect().height + 1}px`; | |
| requestAnimationFrame(() => { | |
| canvas.style.height = height; | |
| }); | |
| }; | |
| const init = () => { | |
| // Userscript injection can lose the race against the site's inline | |
| // drawTreemap() call (e.g. cached bundle on a normal reload), leaving the | |
| // unit cache painted in stock colors. Always repaint once on load. | |
| repaint(); | |
| new MutationObserver(repaint).observe(document.documentElement, { | |
| attributes: true, | |
| attributeFilter: ['data-theme'], | |
| }); | |
| window | |
| .matchMedia?.('(prefers-color-scheme: dark)') | |
| .addEventListener('change', repaint); | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| init(); | |
| } | |
| })(); |
marijnvdwerf
commented
Sep 8, 2026
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment