ShaderToy Layout Resizer

Add a resizable handle between left and right sections on ShaderToy

// ==UserScript==
// @name         ShaderToy Layout Resizer
// @version      1.0
// @description  Add a resizable handle between left and right sections on ShaderToy
// @author       seofernando25
// @match        https://www.shadertoy.com/view/*
// @license MIT
// @namespace https://greasyfork.org/users/1533483
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const HANDLE_WIDTH = 8;
    const MIN_LEFT_WIDTH = 300;
    const MIN_RIGHT_WIDTH = 400;
    const STORAGE_KEY = 'shadertoy_left_width';

    // Wait for DOM to be ready
    function waitForElement(selector, timeout = 5000) {
        return new Promise((resolve, reject) => {
            const element = document.querySelector(selector);
            if (element) {
                resolve(element);
                return;
            }

            const observer = new MutationObserver((mutations, obs) => {
                const element = document.querySelector(selector);
                if (element) {
                    obs.disconnect();
                    resolve(element);
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            setTimeout(() => {
                observer.disconnect();
                reject(new Error(`Element ${selector} not found within ${timeout}ms`));
            }, timeout);
        });
    }

    // Initialize resizer
    async function initResizer() {
        try {
            // Wait for blocks to exist
            const block0 = await waitForElement('.block0');
            const block1 = await waitForElement('.block1');
            const block2 = await waitForElement('.block2');
            const container = block0.parentElement;

            if (!container) {
                console.error('Container not found');
                return;
            }

            // Ensure container is using grid layout
            const containerStyle = window.getComputedStyle(container);
            if (containerStyle.display !== 'grid') {
                console.log('Container display is not grid:', containerStyle.display);
                // Force grid layout
                container.style.display = 'grid';
                container.style.gridTemplateRows = 'auto auto';
                container.style.gridGap = '32px';
                container.style.alignItems = 'start';
            }

            // Override media query grid-template-columns
            // We'll set it dynamically in setWidths, but clear any existing value first
            // Create a style element to override media queries with !important
            // Remove any existing style we may have added
            const existingStyle = document.getElementById('shadertoy-resizer-style');
            if (existingStyle) {
                existingStyle.remove();
            }
            const style = document.createElement('style');
            style.id = 'shadertoy-resizer-style';
            style.textContent = `
                .container {
                    grid-template-columns: var(--shadertoy-left-width, 50%) 1fr !important;
                }
            `;
            document.head.appendChild(style);

            // Ensure block0 and block2 are in first column, block1 is in second
            block0.style.gridColumn = '1';
            block2.style.gridColumn = '1';
            block1.style.gridColumn = '2';

            // Create resize handle
            const handle = document.createElement('div');
            handle.id = 'shadertoy-resize-handle';
            handle.style.cssText = `
                position: absolute;
                left: 0;
                top: 0;
                bottom: 0;
                width: ${HANDLE_WIDTH}px;
                cursor: col-resize;
                background: rgba(128, 128, 128, 0.2);
                z-index: 1000;
                transition: background 0.2s;
                display: flex;
                align-items: center;
                justify-content: center;
            `;

            // Add visual indicator (vertical line)
            const indicator = document.createElement('div');
            indicator.style.cssText = `
                width: 2px;
                height: 30px;
                background: rgba(0, 0, 0, 0.3);
                border-left: 1px solid rgba(255, 255, 255, 0.3);
                border-right: 1px solid rgba(255, 255, 255, 0.3);
                pointer-events: none;
            `;
            handle.appendChild(indicator);

            // Hover effect
            handle.addEventListener('mouseenter', () => {
                handle.style.background = 'rgba(128, 128, 128, 0.4)';
            });
            handle.addEventListener('mouseleave', () => {
                handle.style.background = 'rgba(128, 128, 128, 0.2)';
            });

            // Insert handle and make container position relative
            if (getComputedStyle(container).position === 'static') {
                container.style.position = 'relative';
            }
            container.appendChild(handle);

            // Get saved width or use default (50% of viewport)
            let leftWidth = localStorage.getItem(STORAGE_KEY);
            if (leftWidth) {
                leftWidth = parseInt(leftWidth, 10);
            } else {
                // Default: use current block0 width or 50% of container
                const currentWidth = block0.offsetWidth || Math.floor(container.offsetWidth / 2);
                leftWidth = currentWidth;
            }

            // Apply widths using CSS Grid template columns
            function setWidths(leftW) {
                const containerWidth = container.offsetWidth;
                let rightWidth = containerWidth - leftW - HANDLE_WIDTH;

                // Ensure minimum widths
                if (leftW < MIN_LEFT_WIDTH) {
                    leftW = MIN_LEFT_WIDTH;
                }
                if (rightWidth < MIN_RIGHT_WIDTH) {
                    leftW = containerWidth - MIN_RIGHT_WIDTH - HANDLE_WIDTH;
                    rightWidth = containerWidth - leftW - HANDLE_WIDTH;
                }

                // Set grid-template-columns on the container
                // This is the key: we override the media query columns with our dynamic value
                // Use 2 columns: fixed width for left, 1fr for right (which takes remaining space)
                // Inline style has higher specificity than media queries (unless they use !important)
                container.style.gridTemplateColumns = `${leftW}px 1fr`;
                // Also update CSS variable for the style sheet
                container.style.setProperty('--shadertoy-left-width', `${leftW}px`);

                // Ensure blocks are in correct columns
                block0.style.gridColumn = '1';
                block2.style.gridColumn = '1';
                block1.style.gridColumn = '2';

                // Position handle absolutely between the two columns
                const block0Rect = block0.getBoundingClientRect();
                const containerRect = container.getBoundingClientRect();
                handle.style.left = `${block0Rect.right - containerRect.left}px`;

                // Save width
                localStorage.setItem(STORAGE_KEY, leftW.toString());
            }

            // Track initial container width for percentage-based resizing
            let initialContainerWidth = container.offsetWidth;

            // Apply initial width
            setWidths(leftWidth);

            // Drag functionality
            let isDragging = false;
            let startX = 0;
            let startLeftWidth = 0;

            handle.addEventListener('mousedown', (e) => {
                e.preventDefault();
                isDragging = true;
                startX = e.clientX;
                
                // Get current left width from block0 or grid
                const block0Rect = block0.getBoundingClientRect();
                startLeftWidth = block0Rect.width;

                document.body.style.cursor = 'col-resize';
                document.body.style.userSelect = 'none';
            });

            document.addEventListener('mousemove', (e) => {
                if (!isDragging) return;

                const deltaX = e.clientX - startX;
                const newLeftWidth = startLeftWidth + deltaX;
                setWidths(newLeftWidth);
            });

            document.addEventListener('mouseup', () => {
                if (isDragging) {
                    isDragging = false;
                    document.body.style.cursor = '';
                    document.body.style.userSelect = '';
                }
            });

            // Handle window resize - maintain percentage
            let resizeTimeout;
            window.addEventListener('resize', () => {
                clearTimeout(resizeTimeout);
                resizeTimeout = setTimeout(() => {
                    const currentContainerWidth = container.offsetWidth;
                    // Update if container actually resized significantly
                    if (Math.abs(currentContainerWidth - initialContainerWidth) > 10) {
                        const savedWidth = parseInt(localStorage.getItem(STORAGE_KEY), 10);
                        if (savedWidth && initialContainerWidth > 0) {
                            // Maintain percentage instead of fixed pixel width
                            const percentage = savedWidth / initialContainerWidth;
                            const newWidth = Math.floor(currentContainerWidth * percentage);
                            setWidths(newWidth);
                            // Update initial width for next resize
                            initialContainerWidth = currentContainerWidth;
                        } else {
                            // Fallback: recalculate from current block0 width
                            initialContainerWidth = currentContainerWidth;
                            const block0Width = block0.offsetWidth;
                            if (block0Width > 0) {
                                setWidths(block0Width);
                            }
                        }
                    }
                }, 100);
            });

            console.log('ShaderToy Resizer initialized');

        } catch (error) {
            console.error('Error initializing ShaderToy Resizer:', error);
        }
    }

    // Start initialization when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initResizer);
    } else {
        initResizer();
    }

})();