image color viewer.js

Adds context menu options to view an image in a new tab with dynamic background controls, and to change the background directly behind the image on the current page (white, black, transparent, reset).

Verze ze dne 05. 07. 2025. Zobrazit nejnovější verzi.

// ==UserScript==
// @name  image color viewer.js
// @namespace    github.com/annaprojects
// @version      1.3
// @description  Adds context menu options to view an image in a new tab with dynamic background controls, and to change the background directly behind the image on the current page (white, black, transparent, reset).
// @author       annaroblox
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_openInTab
// ==/UserScript==

(function() {
    'use strict';

    // Variable to store the URL of the image that was right-clicked.
    let imageUrl = null;
    // Variable to store the actual DOM image element that was right-clicked.
    let currentImageElement = null;

    // Define a class name for our custom wrapper elements to easily identify them.
    const WRAPPER_CLASS = 'gm-image-bg-wrapper';

    /**
     * Event listener for the 'contextmenu' event.
     * When a user right-clicks, this function checks if the target is an image.
     * If it is, both the image's source URL and the image DOM element are stored.
     * Otherwise, both are reset to null.
     */
    document.addEventListener('contextmenu', function(e) {
        if (e.target.tagName === 'IMG') {
            imageUrl = e.target.src;
            currentImageElement = e.target; // Store the actual image element
        } else {
            imageUrl = null;
            currentImageElement = null; // Clear both if a non-image element is right-clicked
        }
    }, true); // Use capture phase to ensure this runs before other context menu handlers

    /**
     * Registers a context menu command for "View Image in New Tab".
     * When this command is selected, if an image URL is available, it opens the image
     * viewer in a new tab with an initial white background. The new tab will have its
     * own controls to change the background.
     */
    GM_registerMenuCommand("View Image in New Tab", function() {
        if (imageUrl) {
            // Open the image viewer in a new tab, defaulting to a white background.
            // The new tab will contain controls to change its background dynamically.
            openImageViewer(imageUrl, 'white');
        }
    });

    /**
     * Registers a context menu command for "Set Image BG (White)".
     * When selected, this applies a white background directly behind the clicked image
     * on the current page.
     */
    GM_registerMenuCommand("Set Image BG (White)", function() {
        if (currentImageElement) {
            setImageBackground(currentImageElement, 'white');
        }
    });

    /**
     * Registers a context menu command for "Set Image BG (Black)".
     * When selected, this applies a black background directly behind the clicked image
     * on the current page.
     */
    GM_registerMenuCommand("Set Image BG (Black)", function() {
        if (currentImageElement) {
            setImageBackground(currentImageElement, 'black');
        }
    });

    /**
     * Registers a context menu command for "Set Image BG (Transparent)".
     * When selected, this applies a checkerboard background directly behind the clicked image
     * on the current page, simulating transparency.
     */
    GM_registerMenuCommand("Set Image BG (Transparent)", function() {
        if (currentImageElement) {
            setImageBackground(currentImageElement, 'transparent');
        }
    });

    /**
     * Registers a context menu command for "Reset Image BG".
     * When selected, this removes any custom background wrapper applied to the clicked image,
     * returning it to its original state on the page.
     */
    GM_registerMenuCommand("Reset Image BG", function() {
        if (currentImageElement) {
            resetImageBackground(currentImageElement);
        }
    });

    /**
     * Applies a background color or pattern behind the given image element on the current page.
     * It either creates a new wrapper div or updates an existing one.
     *
     * @param {HTMLElement} imageElement - The <img> element to apply the background to.
     * @param {string} bgColor - The desired background color ('white', 'black', or 'transparent').
     */
    function setImageBackground(imageElement, bgColor) {
        const parent = imageElement.parentNode;
        let wrapper = null;

        // Check if the image is already inside one of our wrappers
        if (parent && parent.classList && parent.classList.contains(WRAPPER_CLASS)) {
            wrapper = parent;
        } else {
            // Create a new wrapper div
            wrapper = document.createElement('div');
            wrapper.classList.add(WRAPPER_CLASS); // Mark it with our custom class
            wrapper.style.display = 'inline-flex'; // Use inline-flex to wrap content and allow centering
            wrapper.style.alignItems = 'center';
            wrapper.style.justifyContent = 'center';
            wrapper.style.padding = '10px'; // Padding to show the background
            wrapper.style.borderRadius = '8px'; // Rounded corners for the background
            wrapper.style.boxShadow = '0 4px 10px rgba(0,0,0,0.2)'; // Subtle shadow
            wrapper.style.transition = 'background-color 0.3s ease, background-image 0.3s ease'; // Smooth transitions
            wrapper.style.lineHeight = '0'; // Prevent extra space below image due to line-height

            // Insert the wrapper before the image, then append the image to the wrapper
            parent.insertBefore(wrapper, imageElement);
            wrapper.appendChild(imageElement);
        }

        // Apply the chosen background style to the wrapper
        if (bgColor === 'transparent') {
            wrapper.style.backgroundImage = 'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)';
            wrapper.style.backgroundSize = '20px 20px';
            wrapper.style.backgroundPosition = '0 0, 0 10px, 10px -10px, -10px 0px';
            wrapper.style.backgroundColor = ''; // Clear solid color if any
        } else {
            wrapper.style.backgroundImage = 'none';
            wrapper.style.backgroundSize = 'auto';
            wrapper.style.backgroundPosition = 'auto';
            wrapper.style.backgroundColor = bgColor;
        }
    }

    /**
     * Resets the background of an image by removing the custom wrapper.
     *
     * @param {HTMLElement} imageElement - The <img> element whose background wrapper should be removed.
     */
    function resetImageBackground(imageElement) {
        const parent = imageElement.parentNode;

        // Check if the parent is our custom wrapper
        if (parent && parent.classList && parent.classList.contains(WRAPPER_CLASS)) {
            const grandParent = parent.parentNode;
            if (grandParent) {
                // Move the image back to its original parent's position
                grandParent.insertBefore(imageElement, parent);
                // Remove the wrapper div
                grandParent.removeChild(parent);
            }
        }
    }

    /**
     * Opens a new tab with the image viewer HTML content.
     * The HTML includes the image and controls to change the background color dynamically.
     *
     * @param {string} url - The URL of the image to display.
     * @param {string} initialBgColor - The initial background color ('white', 'black', or 'transparent').
     */
    function openImageViewer(url, initialBgColor) {
        // Construct the HTML content for the new image viewer tab.
        // It includes basic styling, the image, and JavaScript functions for changing the background.
        const imageViewerHTML = `
            <!DOCTYPE html>
            <html>
            <head>
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Image Viewer</title>
                <!-- Link to Google Fonts for Inter font -->
                <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
                <style>
                    /* Basic body styling for centering content and smooth background transitions */
                    body {
                        background-color: ${initialBgColor}; /* Set the initial background color */
                        margin: 0;
                        display: flex;
                        flex-direction: column; /* Stack controls and image vertically */
                        justify-content: center;
                        align-items: center;
                        min-height: 100vh; /* Full viewport height */
                        transition: background-color 0.3s ease; /* Smooth transition for background changes */
                        font-family: 'Inter', sans-serif; /* Apply Inter font */
                        color: #333; /* Default text color */
                        overflow: hidden; /* Prevent scrollbars if image is slightly larger */
                    }

                    /* Styling for the control buttons container */
                    .controls {
                        position: fixed; /* Keep controls visible even when scrolling (though this page won't scroll much) */
                        top: 20px; /* Position from the top */
                        left: 50%; /* Center horizontally */
                        transform: translateX(-50%); /* Adjust for true centering */
                        background-color: rgba(255, 255, 255, 0.9); /* Semi-transparent white background for controls */
                        padding: 10px 20px;
                        border-radius: 10px; /* Rounded corners for the control panel */
                        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); /* Subtle shadow for depth */
                        display: flex; /* Use flexbox for button layout */
                        gap: 10px; /* Space between buttons */
                        z-index: 1000; /* Ensure controls are on top of other content */
                    }

                    /* Styling for the individual background change buttons */
                    .controls button {
                        padding: 8px 15px;
                        border: none; /* No default border */
                        border-radius: 5px; /* Rounded corners for buttons */
                        cursor: pointer; /* Indicate interactivity */
                        font-size: 14px;
                        font-weight: 500;
                        transition: background-color 0.2s ease, transform 0.1s ease; /* Smooth hover and click effects */
                        background-color: #007bff; /* Primary blue color for buttons */
                        color: white; /* White text on buttons */
                        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Small shadow for buttons */
                    }

                    /* Hover effect for buttons */
                    .controls button:hover {
                        background-color: #0056b3; /* Darker blue on hover */
                        transform: translateY(-1px); /* Slight lift effect */
                    }

                    /* Active (clicked) effect for buttons */
                    .controls button:active {
                        transform: translateY(1px); /* Slight press effect */
                    }

                    /* Styling for the image itself */
                    img {
                        max-width: 95%; /* Image takes up to 95% of the viewport width */
                        max-height: 95vh; /* Image takes up to 95% of the viewport height */
                        object-fit: contain; /* Ensures the entire image is visible within its bounds */
                        border-radius: 8px; /* Rounded corners for the image */
                        box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25); /* More prominent shadow for the image */
                        margin-top: 80px; /* Push image down to clear controls */
                    }

                    /* Responsive adjustments for smaller screens */
                    @media (max-width: 600px) {
                        .controls {
                            flex-direction: column; /* Stack buttons vertically on small screens */
                            width: 80%; /* Wider control panel */
                            top: 10px;
                        }
                        .controls button {
                            width: 100%; /* Full width buttons */
                        }
                        img {
                            margin-top: 150px; /* Adjust margin for stacked controls */
                        }
                    }
                </style>
            </head>
            <body>
                <!-- Control panel for changing background colors -->
                <div class="controls">
                    <button onclick="changeBg('white')">White Background</button>
                    <button onclick="changeBg('black')">Black Background</button>
                    <button onclick="changeBg('transparent')">Transparent Background</button>
                </div>

                <!-- The image display area -->
                <img src="${url}" alt="Image Viewer">

                <script>
                    /**
                     * JavaScript function to dynamically change the body's background.
                     * This function is called by the buttons in the control panel within the new tab.
                     * @param {string} color - The desired background color ('white', 'black', or 'transparent').
                     */
                    function changeBg(color) {
                        const body = document.body;
                        if (color === 'transparent') {
                            // For a 'transparent' background, apply a checkerboard pattern
                            // This simulates transparency on a webpage that doesn't have a true transparent layer beneath it.
                            body.style.backgroundImage = 'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)';
                            body.style.backgroundSize = '20px 20px'; // Size of each checkerboard square
                            body.style.backgroundPosition = '0 0, 0 10px, 10px -10px, -10px 0px'; // Offset for checkerboard pattern
                            body.style.backgroundColor = ''; // Clear any solid background color
                            body.style.color = '#333'; // Default text color for checkerboard
                        } else {
                            // For solid colors, clear the checkerboard and set the solid color
                            body.style.backgroundImage = 'none'; // Remove checkerboard pattern
                            body.style.backgroundSize = 'auto';
                            body.style.backgroundPosition = 'auto';
                            body.style.backgroundColor = color; // Set the solid background color
                            // Adjust text color for readability
                            body.style.color = (color === 'black') ? '#eee' : '#333';
                        }
                    }
                </script>
            </body>
            </html>
        `;

        // Create a Blob from the HTML string. This allows us to create a URL for the HTML content.
        const blob = new Blob([imageViewerHTML], { type: 'text/html' });
        // Create a URL for the Blob. This URL can be opened in a new tab.
        const blobURL = URL.createObjectURL(blob);

        // Open the new tab using Tampermonkey's GM_openInTab function.
        GM_openInTab(blobURL, { active: true, insert: false });

        // Optional: Revoke the Blob URL after a short delay.
        // This frees up memory, as the browser will have already loaded the content from the URL.
        setTimeout(() => {
            URL.revokeObjectURL(blobURL);
        }, 1000);
    }

})();