Skip to content

Instantly share code, notes, and snippets.

@jimmckeeth
Last active August 24, 2025 20:54
Show Gist options
  • Save jimmckeeth/20f98a53dcfb52cb8eae7e76db501f36 to your computer and use it in GitHub Desktop.
Save jimmckeeth/20f98a53dcfb52cb8eae7e76db501f36 to your computer and use it in GitHub Desktop.
Shadertoy View Splitter - JavaScript to add a splitter on the Shadertoy view page.

This JavaScript injects a splitter on the Shadertoy view page to make it easy to resize the view of the shader and the code editing area. I made a pull request to the unoffical Shadertoy plugin to include this feature.

Just run the script in your browser's console window on a Shadertoy view page. Double-click the splitter to revert the position, and right-click it to remove it.

screenshot of the splitter
// https://gist.github.com/jimmckeeth/20f98a53dcfb52cb8eae7e76db501f36/
/*
Just run the script in your browser's console window on a Shadertoy view page.
Double-click the splitter to revert the position, and right-click it to remove it.
*/
(function () {
const container = document.querySelector('.container');
if (!container) return console.error('No .container found');
// Require at least two children to split
const kids = Array.from(container.children);
if (kids.length < 2) return console.error('.container needs at least two child elements');
const splitterWidth = 4;
const minLeft = 80; // px, minimum left pane
const minRight = 80; // px, minimum right pane
// Cache original settings so this is reversible
const original = {
display: container.style.display,
gridTemplateColumns: container.style.gridTemplateColumns,
position: container.style.position,
};
// Make sure it’s a grid and positionable
const cs = getComputedStyle(container);
if (cs.display !== 'grid') container.style.display = 'grid';
if (cs.position === 'static' || !cs.position) container.style.position = 'relative';
// Pin first two children to columns 1 and 3 (we’ll create 3 tracks)
const leftEl = kids[0];
const rightEl = kids[1];
leftEl.style.gridColumn = '1';
rightEl.style.gridColumn = '3';
// Use the current template if it exists; otherwise assume two cols.
// We need to split by spaces but NOT inside parentheses (e.g. minmax(), repeat()).
function splitTracks(str) {
const out = [];
let buf = '';
let depth = 0;
for (const ch of str.trim()) {
if (ch === '(') depth++;
if (ch === ')') depth = Math.max(0, depth - 1);
if (ch === ' ' && depth === 0) {
if (buf) { out.push(buf); buf = ''; }
} else {
buf += ch;
}
}
if (buf) out.push(buf);
return out;
}
let startCols = cs.gridTemplateColumns.trim();
let leftTrack = '1fr', rightTrack = '1fr';
if (startCols) {
const tokens = splitTracks(startCols);
if (tokens.length >= 2) {
leftTrack = tokens[0];
}
}
// Create three tracks: left | splitter | right (preserve original left/right)
container.style.gridTemplateColumns = `${leftTrack} ${splitterWidth}px auto`;
container.style.gridGap = '14px';
// If a previous splitter exists, remove it first
const old = container.querySelector('#grid-splitter');
if (old) old.remove();
// Build the visible handle (absolute so it always catches events)
const handle = document.createElement('div');
handle.id = 'grid-splitter';
Object.assign(handle.style, {
position: 'absolute',
top: '0',
bottom: '0',
width: splitterWidth + 'px',
left: '50%',
transform: 'translateX(-50%)',
cursor: 'col-resize',
background: '#777777',
opacity: '1',
transition: 'background 0.15s ease, opacity 1s ease',
zIndex: '9999',
userSelect: 'none'
});
handle.addEventListener('mouseenter', () => {
handle.style.background = '#777777';
handle.style.opacity = '1';
});
handle.addEventListener('mouseleave', () => {
handle.style.background = '#777777';
handle.style.opacity = '0';
});
container.appendChild(handle);
// Show for 1s, then fade out over 1s
setTimeout(() => { handle.style.opacity = '0'; }, 500);
// Floating readout showing column widths during drag
const readout = document.createElement('div');
Object.assign(readout.style, {
position: 'fixed',
top: '0',
left: '0',
transform: 'translate(12px, 12px)', // offset from pointer via JS position
padding: '4px 8px',
font: '12px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif',
background: 'rgba(0,0,0,0.8)',
color: '#fff',
borderRadius: '6px',
pointerEvents: 'none',
zIndex: '10000',
display: 'none',
whiteSpace: 'nowrap',
boxShadow: '0 2px 6px rgba(0,0,0,0.35)'
});
document.body.appendChild(readout);
// Utility to set columns from an x position (px from container left)
function setColumnsFromX(x) {
const rect = container.getBoundingClientRect();
const total = rect.width;
const left = Math.max(minLeft, Math.min(x, total - minRight - splitterWidth));
const right = Math.max(minRight, total - left - splitterWidth);
container.style.gridTemplateColumns = `${left}px ${splitterWidth}px auto`;
handle.style.left = (left + splitterWidth / 2) + 'px';
return { left, right, total };
}
// Initialize handle near the existing column boundary if possible
(function tryAlignToCurrentColumns() {
const parts = splitTracks(startCols);
const rect = container.getBoundingClientRect();
const total = rect.width;
let leftGuess = total / 2;
function parseLen(val, fallback) {
if (val.endsWith('px')) return parseFloat(val);
if (val.endsWith('fr')) {
const totalFr = parts
.filter(v => v.endsWith('fr'))
.map(v => parseFloat(v))
.reduce((a, b) => a + b, 0);
if (totalFr === 0) return fallback;
return total * (parseFloat(val) / totalFr);
}
if (val.endsWith('%')) return total * (parseFloat(val) / 100);
return fallback;
}
if (parts.length >= 2) {
const guess = parseLen(parts[0], leftGuess);
if (isFinite(guess) && guess > 0 && guess < total) leftGuess = guess;
}
setColumnsFromX(leftGuess);
})();
// --- Drag handlers ---
let dragging = false;
let dragOffset = 0;
function updateReadout(clientX, clientY, sizes) {
if (!sizes) return;
const { left, right, total } = sizes;
const lp = Math.round((left / total) * 100);
const rp = Math.round((right / total) * 100);
readout.textContent = `${Math.round(left)}px (${lp}%) | ${Math.round(right)}px (${rp}%)`;
readout.style.left = clientX + 'px';
readout.style.top = clientY + 'px';
}
function onDown(e) {
dragging = true;
handle.style.opacity = '1';
document.body.style.cursor = 'col-resize';
e.preventDefault();
const rect = handle.getBoundingClientRect();
dragOffset = e.clientX - (rect.left + rect.width / 2);
readout.style.display = 'block';
}
function onMove(e) {
if (!dragging) return;
handle.style.opacity = '1';
const cRect = container.getBoundingClientRect();
const sizes = setColumnsFromX(e.clientX - cRect.left - dragOffset);
updateReadout(e.clientX, e.clientY, sizes);
}
function onUp() {
if (!dragging) return;
dragging = false;
document.body.style.cursor = '';
readout.style.display = 'none';
handle.style.opacity = '0';
}
handle.addEventListener('mousedown', onDown);
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
// Touch support
handle.addEventListener('touchstart', (e) => {
const t = e.touches[0];
dragging = true;
document.body.style.cursor = 'col-resize';
const rect = handle.getBoundingClientRect();
dragOffset = t.clientX - (rect.left + rect.width / 2);
readout.style.display = 'block';
e.preventDefault();
}, { passive: false });
window.addEventListener('touchmove', (e) => {
if (!dragging) return;
const t = e.touches[0];
const cRect = container.getBoundingClientRect();
const sizes = setColumnsFromX(t.clientX - cRect.left - dragOffset);
updateReadout(t.clientX, t.clientY, sizes);
}, { passive: false });
window.addEventListener('touchend', onUp);
// Double-click to reset to 50/50
handle.addEventListener('dblclick', () => {
const rect = container.getBoundingClientRect();
const sizes = setColumnsFromX((rect.width - splitterWidth) / 2);
// Center readout briefly at handle
updateReadout(rect.left + rect.width / 2, rect.top + 24, sizes);
});
// Right-click (context menu) on the bar should remove it and restore layout
function removeSplitter() {
// Clean listeners
handle.removeEventListener('mousedown', onDown);
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
handle.removeEventListener('touchstart', onDown);
window.removeEventListener('touchmove', onMove);
window.removeEventListener('touchend', onUp);
// Remove UI elements
if (handle.parentNode) handle.parentNode.removeChild(handle);
if (readout.parentNode) readout.parentNode.removeChild(readout);
// console.log('Splitter removed.');
}
handle.addEventListener('contextmenu', (e) => {
e.preventDefault();
removeSplitter();
});
// console.log('Splitter attached. Drag the visible bar to resize. Right‑click it to remove.');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment