Picmix Scripts

Image Adjustments+

i have a few more scripts i'm working on, but for now i thought people might like this one. to skip to the download, click here.

if you don't already have a user script manager, you'll need one to add my script!

how do i install it?


if you haven't already, install Tampermonkey (or Greasemonkey) from the links above.

download (or paste into manager)

you can either download my script from this link, or you can copy the code and paste it into the manager yourself.

// ==UserScript==
// @name        β™ͺβ€§β‚ŠΛš pixmix image adjustments+ v0.5
// @namespace   http://tampermonkey.net/
// @version     0.5b
// @description adds 'saturation', 'contrast', and 'luminosity' sliders to stamps and fixes tint for text objects
// @author      etoilee
// @match       *://*.picmix.com/maker/app*
// @grant       none
// ==/UserScript==

(function() {
    'use strict';

    if (typeof maker === 'undefined') {
        console.error("cannot initialize, picmix maker not found...");
        return;
    }

    //css
    const style = document.createElement('style');
    style.innerHTML = `
        .mk-editable-label-wrapper {
            display: flex;
            align-items: center;
        }
        .mk-editable-label {
            cursor: pointer;
            font-size:12px;
            text-decoration: underline;
            text-decoration-style: dashed;
            white-space: nowrap;
        }
        .mk-editable-input {
            width: 60px;
            text-align: center;
            border-radius: 3px;
            box-sizing: border-box;
            display: none;
        }
    `;
    document.head.appendChild(style);

    // html
    function addSlidersToDOM() {
        const optionsPanel = document.getElementById('mk-options-panel');
        if (optionsPanel) {
            const hueSection = optionsPanel.querySelector('[data-section="hue"]');
            const createSliderHTML = (id, labelText, min, max, step, defaultValue, unit) => `
                <section data-section="${id.replace('mk-', '').replace('-input', '')}">
                    <div class="lines">
                        <div class="line">
                            <label>
                                <div class="mk-editable-label-wrapper">
                                    <span class="mk-editable-label" data-target-input="${id}">${labelText}</span>
                                    <input type="number" id="${id}-text" class="mk-editable-input"
                                                min="${min}" max="${max}" step="${step}" value="${defaultValue}" data-unit="${unit}" />
                                </div>
                            </label>
                        </div>
                        <div class="line">
                            <input id="${id}" type="range" min="${min}" max="${max}" step="${step}"
                                        value="${defaultValue}" data-default-value="${defaultValue}" data-unit="${unit}" class="mk-slider" />
                        </div>
                    </div>
                </section>
            `;

            const saturationHTML = createSliderHTML("mk-saturation-input", "Saturation", -100, 200, 1, 100, "%");
            const contrastHTML = createSliderHTML("mk-contrast-input", "Contrast", -100, 200, 1, 100, "%");
            const luminosityHTML = createSliderHTML("mk-luminosity-input", "Luminosity", 0, 200, 1, 100, "%");

            if (hueSection) {
                hueSection.insertAdjacentHTML('afterend', luminosityHTML);
                hueSection.insertAdjacentHTML('afterend', contrastHTML);
                hueSection.insertAdjacentHTML('afterend', saturationHTML);
            } else {
                optionsPanel.insertAdjacentHTML('beforeend', saturationHTML);
                optionsPanel.insertAdjacentHTML('beforeend', contrastHTML);
                optionsPanel.insertAdjacentHTML('beforeend', luminosityHTML);
            }
            console.log("options panel update successful");
        } else {
            console.error("could not update options, not found");
        }
    }

    if (typeof maker.getTextPatternObject === 'undefined') {
        maker.getTextPatternObject = function(_textObject) {
            if (_textObject && _textObject.isText) {
                return _textObject.getObjects().find(o => o.isTextRect);
            }
            return _textObject;
        };
    }

    // get object to apply filters to
    function getFilterTargetObject(obj) {
        if (maker.isTextObject(obj)) {
            // apply filter to the pattern if text object
            const textPatternObject = maker.getTextPatternObject(obj);
            if (textPatternObject && (textPatternObject.isType("image") || textPatternObject.isType("sprite"))) {
                return textPatternObject;
            }
            //otherwise apply filter to object directly
            return obj;
        }
        return obj;
    }

    // get filter
    function updateObjectFilter(object, filterType, propertyName, value) {
        const targetObject = getFilterTargetObject(object);

        // if text pattern is not image
        if (maker.isTextObject(object) && !(targetObject.isType("image") || targetObject.isType("sprite"))) {
            if (filterType === fabric.Image.filters.HueRotation) {
                const hueDegrees = (value + 1) * 180;
                const originalColor = new fabric.Color(object.fill);
                const hsl = originalColor.toHSL();
                hsl.h = hueDegrees;
                object.set('fill', new fabric.Color(hsl).toRgb());
                object.dirty = true;
                maker.canvas.requestRenderAll();
                maker.setDirtyTextObject(object);
                return;
            }
        }


        if (!targetObject || !targetObject.filters) {
            return;
        }

        let filter = targetObject.filters.find(f => f instanceof filterType);

        const defaultValue = filterType.DEFAULT_VALUE !== undefined ? filterType.DEFAULT_VALUE : (propertyName === 'brightness' ? 0 : 1);

        if (value === defaultValue) {
            if (filter) {
                targetObject.filters = targetObject.filters.filter(f => f !== filter);
            }
        } else {
            if (!filter) {
                filter = new filterType();
                targetObject.filters.push(filter);
            }
            filter[propertyName] = value;
        }

        targetObject.filters = targetObject.filters.filter(f => f);

        targetObject.applyFilters();
        if (maker.isTextObject(object)) {
            maker.setDirtyTextObject(object);
        }
    }

    // default definitions (maybe change later)
    fabric.Image.filters.Saturation.DEFAULT_VALUE = 1;
    fabric.Image.filters.Contrast.DEFAULT_VALUE = 1;
    fabric.Image.filters.Brightness.DEFAULT_VALUE = 0;
    fabric.Image.filters.HueRotation.DEFAULT_VALUE = 0;


    maker.onSaturationInputChange = function () {
        const activeObjects = maker.canvas.getActiveObjects();
        const value = parseInt($(this).val()) / 100;

        activeObjects.forEach(function (obj) {
            if (maker.isImageObject(obj) || maker.isSpriteObject(obj) || maker.isTextObject(obj)) {
                updateObjectFilter(obj, fabric.Image.filters.Saturation, 'saturation', value);
            }
        });
        maker.canvas.requestRenderAll();
    };

    maker.onContrastInputChange = function () {
        const activeObjects = maker.canvas.getActiveObjects();
        const value = parseInt($(this).val()) / 100;

        activeObjects.forEach(function (obj) {
            if (maker.isImageObject(obj) || maker.isSpriteObject(obj) || maker.isTextObject(obj)) {
                updateObjectFilter(obj, fabric.Image.filters.Contrast, 'contrast', value);
            }
        });
        maker.canvas.requestRenderAll();
    };

    maker.onLuminosityInputChange = function () {
        const activeObjects = maker.canvas.getActiveObjects();
        const value = (parseInt($(this).val()) - 100) / 100;
        activeObjects.forEach(function (obj) {
            if (maker.isImageObject(obj) || maker.isSpriteObject(obj) || maker.isTextObject(obj)) {
                updateObjectFilter(obj, fabric.Image.filters.Brightness, 'brightness', value);
            }
        });
        maker.canvas.requestRenderAll();
    };

    const originalOnHueInputChange = maker.onHueInputChange;

    maker.onHueInputChange = function (event) {
        const activeObjects = maker.canvas.getActiveObjects();
        let value = (parseInt($(this).val()) / 50) - 1;

        activeObjects.forEach(function (obj) {
            if (maker.isTextObject(obj)) {
                const textPatternObject = maker.getTextPatternObject(obj);
                if (textPatternObject && (textPatternObject.isType("image") || textPatternObject.isType("sprite"))) {
                     updateObjectFilter(obj, fabric.Image.filters.HueRotation, 'rotation', value);
                } else {
                    const originalFill = new fabric.Color(obj.fill);
                    const hsl = originalFill.toHSL();


                    let currentHueDegrees = hsl.h;
                    let rotatedHueDegrees = (currentHueDegrees + (value * 180)) % 360; 
                    if (rotatedHueDegrees < 0) {
                        rotatedHueDegrees += 360;
                    }
                    hsl.h = rotatedHueDegrees;
                    obj.set('fill', new fabric.Color(hsl).toRgb());
                    obj.dirty = true;
                    maker.setDirtyTextObject(obj);
                }
            } else if (maker.isImageObject(obj) || maker.isSpriteObject(obj)) {
                updateObjectFilter(obj, fabric.Image.filters.HueRotation, 'rotation', value);
            }
        });
        maker.canvas.requestRenderAll();
    };


    const originalUpdatePanel = maker.updatePanel;
    maker.updatePanel = function() {
        originalUpdatePanel.apply(this, arguments);

        const activeObjects = maker.canvas.getActiveObjects();
        let applicableObjects = [];

        activeObjects.forEach(obj => {
            if (maker.isImageObject(obj) || maker.isSpriteObject(obj) || maker.isTextObject(obj)) {
                applicableObjects.push(obj);
            }
        });

        const canApplyFilters = applicableObjects.length > 0;

        const saturationInput = $("#mk-saturation-input");
        const contrastInput = $("#mk-contrast-input");
        const luminosityInput = $("#mk-luminosity-input");
        const hueInput = $("#mk-hue-input"); 

        const saturationTextInput = $("#mk-saturation-input-text");
        const contrastTextInput = $("#mk-contrast-input-text");
        const luminosityTextInput = $("#mk-luminosity-input-text");


        const saturationLabel = $('span[data-target-input="mk-saturation-input"]');
        const contrastLabel = $('span[data-target-input="mk-contrast-input"]');
        const luminosityLabel = $('span[data-target-input="mk-luminosity-input"]');


        const saturationSection = $("#mk-context-panel").find("[data-section=\"saturation\"]");
        const contrastSection = $("#mk-context-panel").find("[data-section=\"contrast\"]");
        const luminositySection = $("#mk-context-panel").find("[data-section=\"luminosity\"]");
        const hueSection = $("#mk-context-panel").find("[data-section=\"hue\"]"); // Get the existing hue section

        if (canApplyFilters) {
            let totalSaturation = 0;
            let totalContrast = 0;
            let totalLuminosity = 0;
            let totalHueRotation = 0; 

            applicableObjects.forEach(obj => {
                const targetObject = getFilterTargetObject(obj);

                if (targetObject && targetObject.filters) {
                    let objSaturation = fabric.Image.filters.Saturation.DEFAULT_VALUE;
                    let objContrast = fabric.Image.filters.Contrast.DEFAULT_VALUE;
                    let objLuminosity = fabric.Image.filters.Brightness.DEFAULT_VALUE;
                    let objHueRotation = fabric.Image.filters.HueRotation.DEFAULT_VALUE;

                    targetObject.filters.forEach(filter => {
                        if (filter instanceof fabric.Image.filters.Saturation) {
                            objSaturation = filter.saturation;
                        } else if (filter instanceof fabric.Image.filters.Contrast) {
                            objContrast = filter.contrast;
                        } else if (filter instanceof fabric.Image.filters.Brightness) {
                            objLuminosity = filter.brightness;
                        } else if (filter instanceof fabric.Image.filters.HueRotation) {
                            objHueRotation = filter.rotation;
                        }
                    });
                    totalSaturation += objSaturation;
                    totalContrast += objContrast;
                    totalLuminosity += objLuminosity;
                    totalHueRotation += objHueRotation;
                } else if (maker.isTextObject(obj) && !targetObject.isType("image") && !targetObject.isType("sprite")) {
                    // for plain text with no fill (no use since plain text is always black?)
                    totalHueRotation += fabric.Image.filters.HueRotation.DEFAULT_VALUE;
                }
            });

            const avgSaturation = totalSaturation / applicableObjects.length;
            const avgContrast = totalContrast / applicableObjects.length;
            const avgLuminosity = totalLuminosity / applicableObjects.length;
            const avgHueRotation = totalHueRotation / applicableObjects.length;


            saturationInput.val(Math.round(avgSaturation * 100));
            contrastInput.val(Math.round(avgContrast * 100));
            luminosityInput.val(Math.round((avgLuminosity + 1) * 100));
            hueInput.val(Math.round((avgHueRotation + 1) * 50));

            saturationInput.prop("disabled", false);
            contrastInput.prop("disabled", false);
            luminosityInput.prop("disabled", false);
            hueInput.prop("disabled", false); 

            saturationTextInput.prop("disabled", false);
            contrastTextInput.prop("disabled", false);
            luminosityTextInput.prop("disabled", false);


            saturationSection.removeClass('disabled');
            contrastSection.removeClass('disabled');
            luminositySection.removeClass('disabled');
            hueSection.removeClass('disabled'); 


        } else {
            saturationInput.prop("disabled", true).val(saturationInput.data('default-value'));
            contrastInput.prop("disabled", true).val(contrastInput.data('default-value'));
            luminosityInput.prop("disabled", true).val(luminosityInput.data('default-value'));
            hueInput.prop("disabled", true).val(0); 

            saturationTextInput.prop("disabled", true);
            contrastTextInput.prop("disabled", true);
            luminosityTextInput.prop("disabled", true);

            saturationSection.addClass("disabled");
            contrastSection.addClass("disabled");
            luminositySection.addClass("disabled");
            hueSection.addClass("disabled");

            saturationLabel.text('Saturation');
            contrastLabel.text('Contrast');
            luminosityLabel.text('Luminosity');
        }
    };

    // tippy tooltip
    function setupSliderPreview() {
        let sliderPreviewDiv = $('#' + maker.sliderPreviewValueDivId);
        if (sliderPreviewDiv.length === 0) {
            sliderPreviewDiv = $("<div />", {
                'id': maker.sliderPreviewValueDivId,
                'class': 'mk-slider-value-preview'
            }).appendTo($("body"));
        }

        $('#mk-saturation-input, #mk-contrast-input, #mk-luminosity-input, #mk-hue-input').on("input mousedown", function (event) {
            const _0x1d9c08 = $(this);
            const _0x2377f0 = _0x1d9c08.attr("data-unit");
            const _0x3402cd = _0x1d9c08.val() + (_0x2377f0 || '');
            const _0x589990 = _0x1d9c08.offset().top;

            const _0x113797 = maker.mousePosition ? maker.mousePosition.x : event.clientX;

            sliderPreviewDiv.html(_0x3402cd).css({
                'top': _0x589990 + 'px',
                'left': _0x113797 + 'px'
            }).show();
        });

        $('#mk-saturation-input, #mk-contrast-input, #mk-luminosity-input, #mk-hue-input').on("change mouseup", function () {
            sliderPreviewDiv.hide();
        });
    }

    // make labels editable
    function setupEditableLabels() {
        $('.mk-editable-label').each(function() {
            const labelSpan = $(this);
            const sliderId = labelSpan.data('target-input');
            const sliderInput = $(`#${sliderId}`);
            const textInput = $(`#${sliderId}-text`);
            const originalLabelText = labelSpan.text();

            labelSpan.text(originalLabelText);

            labelSpan.on('click', function() {
                if (sliderInput.prop('disabled')) {
                    return;
                }
                labelSpan.hide();
                textInput.val(sliderInput.val() + (sliderInput.data('unit') || ''));
                textInput.show();
                textInput.focus();
                textInput.select();
            });

            const applyValueFromTextInput = function() {
                const rawValue = textInput.val();
                let numericValue = parseFloat(rawValue);

                const unit = sliderInput.data('unit');
                if (unit && rawValue.endsWith(unit)) {
                    numericValue = parseFloat(rawValue.slice(0, -unit.length));
                }

                const min = parseFloat(sliderInput.attr('min'));
                const max = parseFloat(sliderInput.attr('max'));
                const defaultValue = parseFloat(sliderInput.data('default-value'));

                if (isNaN(numericValue)) {
                    numericValue = defaultValue; // fallback
                } else if (numericValue < min) {
                    numericValue = min;
                } else if (numericValue > max) {
                    numericValue = max;
                }

                numericValue = Math.round(numericValue);

                sliderInput.val(numericValue);
                sliderInput.trigger('input'); // trigger input for live updates
                sliderInput.trigger('change'); // trigger change for final update

                // hide the input, display the label
                labelSpan.text(originalLabelText);
                textInput.hide();
                labelSpan.show();
            };

            textInput.on('blur', applyValueFromTextInput);
            textInput.on('keypress', function(e) {
                if (e.which === 13) { // enter key is the default
                    e.preventDefault();
                    applyValueFromTextInput();
                }
            });
        });
    }

    // init
    setTimeout(() => {
        addSlidersToDOM();

        $("#mk-saturation-input").on("input change", maker.onSaturationInputChange);
        $("#mk-contrast-input").on("input change", maker.onContrastInputChange);
        $("#mk-luminosity-input").on("input change", maker.onLuminosityInputChange);
        $("#mk-hue-input").off("input change").on("input change", maker.onHueInputChange); 

        setupSliderPreview();

        setupEditableLabels();

        console.log("picmix image adjustment userscript loaded");

        maker.updatePanel();

    }, 1000); //wait 1000 ms
})();

after downloading, your user script manager (e.g., Tampermonkey) will usually open a new tab asking you to confirm the installation. It will show you the code and ask if you want to install it. click the "Install" button.

Important!

make sure you see the code of the script! it should look something like the one above.

reload the editor (if already open)

once installed, return to or reload the picmix editor page. the script will automatically activate!

how do i use it?


select an object

in the Picmix Maker, add an image, a gif, or a text object to your canvas as normal.

find the sliders

in the top panel, under "Options" you will now see new sections for "Saturation", "Contrast", and "Luminosity".

adjust the sliders

drag the sliders to change the corresponding image properties.

  • Saturation: makes colors more vibrant or desaturated (grayscale).
  • Contrast: adjusts the difference between light and dark areas.
  • Luminosity: controls the overall brightness

finetuning the input

for precise adjustments, click on the underlined "Saturation", "Contrast", or "Luminosity" label above the slider. this will reveal a text input box that looks something like this: where you can type in exact numeric values. press enter or click outside the box to apply.

troubleshooting


  • script not showing?
    • make sure your user script manager (Tampermonkey) is enabled.
    • check if the script is enabled within your manager's dashboard.
    • try refreshing the page
  • sliders disabled?
    • ensure you have an image, sprite, or text object selected on the Picmix canvas. the sliders are only active when an applicable object is selected.
  • still having issues?
    • check your browser's developer console (usually F12 or right-click -> Inspect -> Console) for any error messages.
    • contact the script author (me) for help!

Layers + Favorites

this userscript will make creating picmix a lot less painful. i implemented a layer window and favoriting stamps in the editor. you can download the userscript here.

Demo image of the userscript in action

if you don't already have a userscript manager, you'll need one to add my script

how do i install it?


if you haven't already, install Tampermonkey from the links above.

download (or paste into manager)

you can either download my script from this link, or you can copy the code and paste it into the manager yourself.

// ==UserScript==
// @name         πœ—πœšβ‹†β‚ŠΛšpicmix layer manager + favorites
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  adds a layer panel to picmix for easier stamp management, enables favoriting stamps
// @match        *://*.picmix.com/maker/app*
// @grant        none
// @author      etoilee
// ==/UserScript==

(function() {
    'use strict';

    document.body.dataset.layerPluginActive = 'true';

    function getStampKeyFromSrcThumb(srcThumbUrl) {
        const match = srcThumbUrl.match(/_([a-f0-9]+)\.(png|gif|jpg)$/i);
        if (match &amp;&amp; match[1]) {
            return match[1];
        }
        return null;
    }

    function getObjectName(object) {
        if (object.isType('image') || object.isType('sprite')) {
            const stampInfo = window.maker.stampInfos.find(info =&gt; info.element === object._originalElement);
            if (stampInfo &amp;&amp; stampInfo.name) {
                return stampInfo.name;
            }
            return object.isType('sprite') ? 'Animated Stamp' : 'Stamp';
        } else if (object.isType('i-text')) {
            return object.text;
        }
        return object.type || 'Object';
    }
    
    function createFavoriteButton(stampId, stampKey, isFavorite) {
        const favoriteButton = document.createElement('button');
        const favoritedColor = '#FFD700';
        const unfavoritedColor = 'black';
        favoriteButton.classList.add('stamp-favorite-button');
        favoriteButton.style.cssText = `
            background: none;
            border: none;
            cursor: pointer;
            padding: 0;
            margin-left: auto;
            font-size: 1em;
            color: ${isFavorite ? favoritedColor : unfavoritedColor};
            transition: color 0.2s;
            text-shadow: 0px 0px 2px white;
            line-height: 1;
            z-index: 10;
            position: relative;
        `;
        favoriteButton.innerHTML = isFavorite ? '&amp;#9733;' : '&amp;#9734;'; 
        favoriteButton.title = isFavorite ? 'Unfavorite' : 'Favorite';

        favoriteButton.onclick = function(event) {
            event.stopPropagation();

            const currentIsFavorite = favoriteButton.innerHTML === '&amp;#9733;';

            const originalAddRemoveStampFavorite = window.addRemoveStampFavorite;
            const originalSuccessCallback = window.addRemoveStampFavorite.successCallback;

            window.addRemoveStampFavorite.successCallback = function(json) {
                if (originalSuccessCallback) {
                    originalSuccessCallback(json);
                }

                const targetStampInfo = window.maker.stampInfos.find(info =&gt; String(info.id) === String(stampId) &amp;&amp; String(info.key) === String(stampKey));
                if (targetStampInfo) {
                    targetStampInfo.isFavorite = !currentIsFavorite;
                }

                if (document.getElementById('picmix-layers-panel')) {
                    updateLayerPanel();
                }

                window.addRemoveStampFavorite.successCallback = originalSuccessCallback;
            };

            originalAddRemoveStampFavorite(
                this,
                stampId,
                stampKey,
                '&amp;star;',
                '&amp;starf;',
                'favorite-icon',
                'remove-favorite-icon'
            );
        };
        return favoriteButton;
    }

    const originalDisplayStamps = window.maker.displayStamps;
    window.maker.displayStamps = function(_stamps) {
        _stamps.forEach(stamp =&gt; {
            if (!stamp.key &amp;&amp; stamp.srcThumb) {
                stamp.key = getStampKeyFromSrcThumb(stamp.srcThumb);
            }
            const existingStampInfo = window.maker.stampInfos.find(info =&gt; String(info.id) === String(stamp.id) &amp;&amp; String(info.key) === String(stamp.key));
            if (existingStampInfo) {
                stamp.isFavorite = existingStampInfo.isFavorite;
            } else if (typeof stamp.isFavorite === 'undefined') {
                stamp.isFavorite = false;
            }
        });

        originalDisplayStamps.apply(this, arguments);

        const stampList = document.getElementById('mk-stamps-list');
        if (stampList) {
            stampList.querySelectorAll('.mk-stamp-preview').forEach((stampPreview, index) =&gt; {
                const stampInfo = _stamps[index];
                if (stampInfo &amp;&amp; stampInfo.id &amp;&amp; stampInfo.key) {
                    if (!stampPreview.querySelector('.stamp-favorite-button')) {
                        const favoriteButton = createFavoriteButton(stampInfo.id, stampInfo.key, stampInfo.isFavorite);
                        stampPreview.style.position = 'relative';
                        favoriteButton.style.position = 'absolute';
                        favoriteButton.style.top = '2px';
                        favoriteButton.style.right = '2px';
                        stampPreview.appendChild(favoriteButton);
                    } else {
                        const existingButton = stampPreview.querySelector('.stamp-favorite-button');
                        existingButton.innerHTML = stampInfo.isFavorite ? '&amp;#9733;' : '&amp;#9734;';
                        existingButton.style.color = stampInfo.isFavorite ? 'gold' : '#ccc';
                        existingButton.title = stampInfo.isFavorite ? 'Unfavorite' : 'Favorite';
                    }
                }
            });
        }
    };

    function updateLayerPanel() {
        const layersList = document.getElementById('picmix-layers-list');
        if (!layersList) return;

        layersList.innerHTML = '';

        const objects = window.maker.canvas.getObjects();
        const activeObject = window.maker.canvas.getActiveObject();

        for (let i = objects.length - 1; i &gt;= 0; i--) {
            const obj = objects[i];
            if (obj.isDrawingBackground) continue;

            const listItem = document.createElement('li');
            listItem.dataset.objectId = window.maker.canvas._objects.indexOf(obj);
            listItem.style.cssText = `
                display: flex; align-items: center; padding: 5px; border-radius: 10px;
                background: linear-gradient(to bottom,rgba(255,255,255,1) 0,rgba(240,240,240,1) 100%); cursor: pointer; border: 2px solid transparent;
                position: relative; font-size: 12px; color: #black; overflow: hidden;
            `;

            if (activeObject === obj) {
                listItem.style.borderColor = '#5fb0ee';
                listItem.style.background = 'linear-gradient(to bottom,rgba(227,243,255,1) 0,rgba(189,228,255,1) 100%)';
            }

            const thumbCanvas = document.createElement('canvas');
            thumbCanvas.width = 30; thumbCanvas.height = 30;
            const thumbCtx = thumbCanvas.getContext('2d');
            const originalProps = { scaleX: obj.scaleX, scaleY: obj.scaleY, left: obj.left, top: obj.top, angle: obj.angle, originX: obj.originX, originY: obj.originY };
            const scaleFactor = Math.min(thumbCanvas.width / (obj.width * (obj.scaleX || 1)), thumbCanvas.height / (obj.height * (obj.scaleY || 1)));
            obj.set({ scaleX: scaleFactor, scaleY: scaleFactor, left: thumbCanvas.width / 2, top: thumbCanvas.height / 2, originX: 'center', originY: 'center', angle: 0 });
            obj.render(thumbCtx);
            obj.set(originalProps);
            obj.setCoords();

            const img = document.createElement('img');
            img.src = thumbCanvas.toDataURL();
            img.style.cssText = 'width: 30px; height: 30px; margin-right: 5px; background: #eee; border-radius:10px;';
            listItem.appendChild(img);

            const nameSpan = document.createElement('span');
            nameSpan.textContent = getObjectName(obj);
            nameSpan.style.cssText = 'flex-grow: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;';
            nameSpan.title = getObjectName(obj);
            listItem.appendChild(nameSpan);

            const stampId = window.maker.getStampIdFromObject(obj);
            if (stampId) {
                const stampInfo = window.maker.stampInfos.find(info =&gt; String(info.id) === String(stampId));
                if (stampInfo &amp;&amp; stampInfo.key) {
                    const favoriteButton = createFavoriteButton(stampInfo.id, stampInfo.key, stampInfo.isFavorite);
                    listItem.appendChild(favoriteButton);
                }
            }

            const controlsDiv = document.createElement('div');
            controlsDiv.style.cssText = 'margin-left: 5px; display: flex; gap: 2px;';

            const alphaClipButtonHTML = document.body.dataset.alphaClipPluginActive === 'true'
                ? `&lt;button class=&quot;layer-control-btn alpha-clip-placeholder&quot; data-object-index=&quot;${i}&quot; title=&quot;Toggle Alpha Clip&quot; style=&quot;background: ${obj.clipPath ? '#2e9942' : '#555'}; border: none; color: white; cursor: pointer; padding: 2px 5px; border-radius: 5px; font-size: 10px;&quot;&gt;&amp;#x2702;&lt;/button&gt;`
                : '';


            controlsDiv.innerHTML = `
                ${alphaClipButtonHTML}
                &lt;button class=&quot;layer-control-btn&quot; data-action=&quot;bringForward&quot; title=&quot;Bring Forward&quot; style=&quot;background: dodgerblue; border: none; color: white; cursor: pointer; padding: 2px 5px; border-radius: 5px; font-size: 10px;&quot;&gt;&amp;#9650;&lt;/button&gt;
                &lt;button class=&quot;layer-control-btn&quot; data-action=&quot;sendBackward&quot; title=&quot;Send Backward&quot; style=&quot;background: dodgerblue ; border: none; color: white; cursor: pointer; padding: 2px 5px; border-radius: 5px; font-size: 10px;&quot;&gt;&amp;#9660;&lt;/button&gt;
                &lt;button class=&quot;layer-control-btn&quot; data-action=&quot;remove&quot; title=&quot;Delete Layer&quot; style=&quot;background: indianred; border: none; color: white; cursor: pointer; padding: 2px 5px; border-radius: 5px; font-size: 10px;&quot;&gt;&amp;#x2715;&lt;/button&gt;
            `;
            listItem.appendChild(controlsDiv);

            listItem.addEventListener('click', (event) =&gt; {
                if (event.target.closest('.layer-control-btn, .stamp-favorite-button')) return;
                window.maker.canvas.setActiveObject(obj);
                window.maker.canvas.requestRenderAll();
                updateLayerPanel();
            });

            controlsDiv.querySelector('[data-action=&quot;bringForward&quot;]').addEventListener('click', () =&gt; { obj.bringForward(); updateLayerPanel(); });
            controlsDiv.querySelector('[data-action=&quot;sendBackward&quot;]').addEventListener('click', () =&gt; { obj.sendBackwards(); updateLayerPanel(); });
            controlsDiv.querySelector('[data-action=&quot;remove&quot;]').addEventListener('click', () =&gt; { window.maker.canvas.remove(obj); updateLayerPanel(); });

            layersList.appendChild(listItem);
        }
        document.dispatchEvent(new CustomEvent('layerPanelUpdated'));
    }

    function addLayerPanelHTML() {
        const panelHtml = `
            &lt;style&gt;
                #picmix-layers-panel { position: absolute; right: 10px; top: 100px; width: 250px; background: linear-gradient(101deg,rgba(223, 30, 126, 0.52) 0%, rgba(247, 141, 52, 0.19) 99%); border-radius: 15px; color: black; font-family: sans-serif; z-index: 1; box-sizing: border-box; }
                #picmix-layers-panel-header { display: flex; justify-content: space-between; border-radius:10px; margin:5px; align-items: center; padding: 5px 10px; background: linear-gradient(180deg, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 1) 38%, rgba(237, 237, 237, 1) 55%, rgb(231 229 229) 69%, rgba(230, 230, 230, 1) 98%); border-bottom: 1px solid #bfbfbf; }
                #picmix-layers-panel h3 { margin: 0; font-size: 14px; text-align: center; flex-grow: 1; }
                #picmix-layers-toggle-btn { background: none; border: none; color: black; font-size: 18px; cursor: pointer; padding: 0 5px; line-height: 1; }
                #picmix-layers-list { list-style: none; padding: 0; margin: 0; max-height: 70vh; overflow-y: auto; display: none; padding: 10px; }
                #picmix-layers-panel.expanded #picmix-layers-list { display: block; }
                #picmix-layers-list::-webkit-scrollbar { width: 8px; }
                #picmix-layers-list::-webkit-scrollbar-track { background: transparent; }
                #picmix-layers-list::-webkit-scrollbar-thumb { background-color: white; border-radius: 4px; border: 2px solid #ddd; }
            &lt;/style&gt;
            &lt;div id=&quot;picmix-layers-panel&quot;&gt;
                &lt;div id=&quot;picmix-layers-panel-header&quot;&gt;
                    &lt;h3&gt;Layers&lt;/h3&gt;
                    &lt;button id=&quot;picmix-layers-toggle-btn&quot;&gt;&amp;#9660;&lt;/button&gt;
                &lt;/div&gt;
                &lt;ul id=&quot;picmix-layers-list&quot;&gt;&lt;/ul&gt;
            &lt;/div&gt;
        `;
        document.body.insertAdjacentHTML('beforeend', panelHtml);

        const toggleButton = document.getElementById('picmix-layers-toggle-btn');
        const layersPanel = document.getElementById('picmix-layers-panel');

        toggleButton.addEventListener('click', () =&gt; {
            const isExpanded = layersPanel.classList.toggle('expanded');
            toggleButton.innerHTML = isExpanded ? '&amp;#9650;' : '&amp;#9660;';
            if (isExpanded) {
                updateLayerPanel();
            }
        });
    }

    function setupCanvasEventHandlers() {
        const canvasEvents = ['object:added', 'selection:created', 'selection:updated', 'selection:cleared', 'object:removed', 'object:modified'];
        canvasEvents.forEach(eventName =&gt; {
            window.maker.canvas.on(eventName, () =&gt; {
                if (document.getElementById('picmix-layers-panel').classList.contains('expanded')) {
                    updateLayerPanel();
                }
            });
        });
    }

    function initialize() {
        if (window.maker &amp;&amp; window.maker.canvas &amp;&amp; !document.getElementById('picmix-layers-panel')) {
            addLayerPanelHTML();
            setupCanvasEventHandlers();
            window.updateLayerManagerPanel = updateLayerPanel;
        } else {
            setTimeout(initialize, 500);
        }
    }

    initialize();
})();

after downloading, your userscript manager (e.g., Tampermonkey) will usually open a new tab asking you to confirm the installation. it will show you the code and ask if you want to install it. click the "Install" button.

Important!

Make sure you see the code of the script! It should look something like the one above.

reload the editor (if already open)

once installed, return to or reload the picMix editor page. the script will automatically activate!

how do i use it?


Step icon

layers menu

after downloading the script, head to the PicMix maker. you'll see a new button that says "Layers", click it to open the layers menu.

after adding a stamp or text, your layers will start to fill up the list. you can now move a layer back and forward using the added buttons. clicking a layer will select it.

you can now delete a layer by clicking the red "x" button. you can also move a selected layer with your arrow keys if you click back into the maker while the move button is active.

if you want to view the full title of a stamp, you can hover over the title and a tooltip will appear.

Step icon

favorites

after downloading the userscript, a star will appear in the top right corner of a stamp. click the star to favorite a stamp, and check out your favorites to see it there.

*please note: the stars will appear empty when first clicking this button, regardless of whether you've favorited it before. this is a limitation of the favoriting feature and is only cosmetic; favoriting and unfavoriting still work!*

if you look at the layer list, you will also be able to favorite and unfavorite from here.

troubleshooting


  • script not showing?
    • make sure your user script manager (Tampermonkey) is enabled.
    • check if the script is enabled within your manager's dashboard.
    • try refreshing the page
  • can't move layer?
    • you can move a selected layer in the menu by selecting it, then clicking the move button. use your arrow keys to move the stamp
  • still having issues?
    • check your browser's developer console (usually F12 or right-click -> Inspect -> Console) for any error messages.
    • contact the script author (me) for help!

PicTweax

it's OUT! check it out here for a full rundown and download.

To Download