diff --git a/ea-scripts/Shade Master.md b/ea-scripts/Shade Master.md index 65c340e..adfaa4b 100644 --- a/ea-scripts/Shade Master.md +++ b/ea-scripts/Shade Master.md @@ -1,17 +1,6 @@ /* -This script modifies colors of selected Excalidraw elements based on modifier keys: - -Color Selection: -- No modifier: Both stroke and background colors -- Shift: Only stroke color -- Cmd/Ctrl(⌘/^): Only background color - -Modification Type: -- No Alt/Opt (⌥): Increase value (lightness/hue/saturation/transparency depending on settings) -- Alt/Opt (⌥): Decrease value - -Action Toggle: -Holding down both SHIFT and CTRL/CMD when clicking the script button will toggle the action. +This script modifies the color lightness/hue/saturation/transparency of selected Excalidraw elements. +Select elements in the scene, then run the script. ```js*/ @@ -20,50 +9,44 @@ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.7.19")) { return; } -const STEP = "Step size"; -const FORMAT = "Color format"; -const ACTION = "Action" +// Main script execution +const allElements = ea.getViewSelectedElements(); +const svgImageElements = allElements.filter(el => + el.type === "image" && ea.getViewFileForImageElement(el)?.extension === "svg" +); + +if(allElements.length === 0) { + new Notice("Select at least one rectangle, ellipse, diamond, line, arrow, freedraw, text or SVG image elment"); + return; +} + +let terminate = false; +const FORMAT = "Color Format"; +const STROKE = "Modify Stroke Color"; +const BACKGROUND = "Modify Background Color" const ACTIONS = ["Hue", "Lightness", "Saturation", "Transparency"]; -const settings = ea.getScriptSettings(); +let settings = ea.getScriptSettings(); //set default values on first run -if(!settings[STEP]) { - settings[STEP] = { - value: 2, - description: "Increment/decrement step size" - }; +if(!settings[STROKE]) { + settings = {}; settings[FORMAT] = { value: "HSL", valueset: ["HSL", "RGB", "HEX"], description: "Output color format" }; - settings[ACTION] = { - value: "Lightness", - valueset: ACTIONS, - description: "Sets which aspect of the color should be modified" - }; + settings[STROKE] = { value: true } + settings[BACKGROUND] = {value: true } ea.setScriptSettings(settings); } -const isMac = ea.DEVICE.isMacOS; -const modKeys = { - alt: isMac ? "⌥" : "ALT", - shift: isMac ? "⇧" : "SHIFT", - ctrl: isMac ? "⌘" : "CTRL" -}; - -function modifyColor(color, modifiers) { +function modifyColor(color, isDecrease, step, action) { if (!color) return null; const cm = ea.getCM(color); if (!cm) return color; - - const step = settings[STEP].value; - const isDecrease = modifiers.altKey; - const action = settings[ACTION].value; let modified = cm; - switch(action) { case "Lightness": modified = isDecrease ? modified.darkerBy(step) : modified.lighterBy(step); @@ -73,6 +56,7 @@ function modifyColor(color, modifiers) { break; case "Transparency": modified = isDecrease ? modified.alphaBy(-step/100) : modified.alphaBy(step/100); + break; default: modified = isDecrease ? modified.desaturateBy(step) : modified.saturateBy(step); } @@ -88,7 +72,25 @@ function modifyColor(color, modifiers) { } } -function showSettingsModal() { +function slider(contentEl, action, min, max, step, invert) { + let prevValue = (max-min)/2; + new ea.obsidian.Setting(contentEl) + .setName(action) + .addSlider(slider => slider + .setLimits(min, max, step) + .setValue(prevValue) + .onChange(async (value) => { + const isDecrease = invert ? value > prevValue : value < prevValue; + const step = Math.abs(value-prevValue); + prevValue = value; + if(step>0) { + await run(isDecrease, step, action); + } + }), + ); +} + +function showModal() { const modal = new ea.obsidian.Modal(app); let dirty = false; @@ -96,37 +98,7 @@ function showSettingsModal() { const { contentEl } = modal; modal.bgOpacity = 0; contentEl.createEl('h2', { text: 'Shade Master' }); - - const instructions = contentEl.createDiv(); - instructions.innerHTML = ` -

Instructions

-

The script button will perform a range of actions depending on how you click and which modifier buttons you press when clicking.

- -

Settings

- `; - - new ea.obsidian.Setting(contentEl) - .setName(STEP) - .setDesc("Increment/decrement step size") - .addText(text => text - .setValue(settings["Step size"].value.toString()) - .onChange(value => { - const val = parseFloat(value); - if (isNaN(val)) { - new Notice("Enter a valid number"); - return; - } - settings["Step size"].value = val; - dirty = true; - })); - + new ea.obsidian.Setting(contentEl) .setName(FORMAT) .setDesc("Output color format") @@ -136,23 +108,38 @@ function showSettingsModal() { "RGB": "RGB", "HEX": "HEX" }) - .setValue(settings["Color format"].value) + .setValue(settings[FORMAT].value) .onChange(value => { - settings["Color format"].value = value; + settings[FORMAT].value = value; dirty = true; - })); + }) + ); new ea.obsidian.Setting(contentEl) - .setName(ACTION) - .setDesc("Sets which aspect of the color should be modified") - .addDropdown(dropdown => dropdown - .addOptions(ACTIONS.reduce((acc, key) => { acc[key] = key; return acc; }, {})) - .setValue(settings[ACTION].value) + .setName(STROKE) + .addToggle(toggle => toggle + .setValue(settings[STROKE].value) .onChange(value => { - settings[ACTION].value = value; + settings[STROKE].value = value; dirty = true; - })); + }) + ); + new ea.obsidian.Setting(contentEl) + .setName(BACKGROUND) + .addToggle(toggle => toggle + .setValue(settings[BACKGROUND].value) + .onChange(value => { + settings[BACKGROUND].value = value; + dirty = true; + }) + ); + + slider(contentEl, "Hue", 0, 200, 1, false); + slider(contentEl, "Saturation", 0, 200, 1, false); + slider(contentEl, "Lightness", 0, 50, 1, false); + slider(contentEl, "Transparency", 0, 100, 1, true); + new ea.obsidian.Setting(contentEl) .addButton(button => button .setButtonText("Close") @@ -163,6 +150,7 @@ function showSettingsModal() { }; modal.onClose = () => { + terminate = true; if (dirty) { ea.setScriptSettings(settings); } @@ -182,7 +170,10 @@ function makeModalDraggable(modalEl) { const header = modalEl.querySelector('.modal-titlebar') || modalEl; // Default to modalEl if no titlebar header.style.cursor = 'move'; - header.addEventListener('mousedown', (e) => { + const onMouseDown = (e) => { + // Ensure the event target isn't an interactive element like slider, button, or input + if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return; + isDragging = true; startX = e.clientX; startY = e.clientY; @@ -194,9 +185,9 @@ function makeModalDraggable(modalEl) { modalEl.style.margin = '0'; // Reset margin to avoid issues modalEl.style.left = `${initialX}px`; modalEl.style.top = `${initialY}px`; - }); + }; - document.addEventListener('mousemove', (e) => { + const onMouseMove = (e) => { if (!isDragging) return; const dx = e.clientX - startX; @@ -204,100 +195,108 @@ function makeModalDraggable(modalEl) { modalEl.style.left = `${initialX + dx}px`; modalEl.style.top = `${initialY + dy}px`; - }); + }; - document.addEventListener('mouseup', () => { + const onMouseUp = () => { isDragging = false; + }; + + header.addEventListener('mousedown', onMouseDown); + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + + // Clean up event listeners on modal close + modalEl.addEventListener('remove', () => { + header.removeEventListener('mousedown', onMouseDown); + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); }); } - -// Main script execution -const allElements = ea.getViewSelectedElements(); -const regularElements = allElements.filter(el => - ["rectangle", "ellipse", "diamond", "line", "freedraw", "text"].includes(el.type) -); -const svgImageElements = allElements.filter(el => - el.type === "image" && ea.getViewFileForImageElement(el)?.extension === "svg" -); - -const modifiers = ea.targetView.modifierKeyDown; - -if((modifiers.ctrlKey || modifiers.metaKey) && modifiers.shiftKey) { - const timestamp = Date.now(); - if(window.ExcalidrawShadeMaster && (timestamp - window.ExcalidrawShadeMaster.timestamp < 350) ) { - window.clearTimeout(window.ExcalidrawShadeMaster.timerID); - delete window.ExcalidrawShadeMaster; - new Notice(`Current action: ${settings[ACTION].value}`); - } else { - if(window.ExcalidrawShadeMaster) { - window.clearTimeout(window.ExcalidrawShadeMaster.timerID); - delete window.ExcalidrawShadeMaster; - } - const timerID = window.setTimeout(()=>{ - delete window.ExcalidrawShadeMaster; - settings[ACTION].value = ACTIONS[(ACTIONS.indexOf(settings[ACTION].value)+1) % ACTIONS.length]; - new Notice (`Action modified to: ${settings[ACTION].value}`); - ea.setScriptSettings(settings); - } ,400); - window.ExcalidrawShadeMaster = { timestamp, timerID }; +isRunning = false; +const queue = []; +function processQueue() { + if (!isRunning && queue.length > 0) { + const [isDecrease, step, action] = queue.shift(); + executeChange(isDecrease, step, action).then(() => { + if (queue.length > 0) processQueue(); + }); } - return; } -// Show settings modal if no elements selected (unchanged) -if (allElements.length === 0) { - showSettingsModal(); - return; +const MAX_QUEUE_SIZE = 100; +function run(isDecrease, step, action) { + if (queue.length >= MAX_QUEUE_SIZE) { + new Notice ("Queue overflow. Dropping task."); + return; + } + queue.push([isDecrease, step, action]); + if (!isRunning) processQueue(); } -// Process regular elements -if (regularElements.length > 0) { - ea.copyViewElementsToEAforEditing(regularElements); +async function executeChange(isDecrease, step, action) { + try { + isRunning = true; + + ea.clear(); + const modifyStroke = settings[STROKE].value; + const modifyBackground = settings[BACKGROUND].value; + + //must reselect after each run since elements change in the scene + const allElements = ea.getViewSelectedElements(); + const regularElements = allElements.filter(el => + ["rectangle", "ellipse", "diamond", "line", "arrow", "freedraw", "text"].includes(el.type) + ); - const modifyStroke = !modifiers.ctrlKey && !modifiers.metaKey; - const modifyBackground = !modifiers.shiftKey && (!modifiers.ctrlKey || !modifiers.metaKey); - - for (const el of ea.getElements()) { - if (modifyStroke && el.strokeColor) { - el.strokeColor = modifyColor(el.strokeColor, modifiers); + // Process regular elements + if (regularElements.length > 0) { + ea.copyViewElementsToEAforEditing(regularElements); + for (const el of ea.getElements()) { + if (modifyStroke && el.strokeColor) { + el.strokeColor = modifyColor(el.strokeColor, isDecrease, step, action); + } + + if (modifyBackground && el.backgroundColor) { + el.backgroundColor = modifyColor(el.backgroundColor, isDecrease, step, action); + } + } + await ea.addElementsToView(false, false); } - if (modifyBackground && el.backgroundColor) { - el.backgroundColor = modifyColor(el.backgroundColor, modifiers); - } - } - - await ea.addElementsToView(false, false); -} - -// Process SVG image elements -if (svgImageElements.length > 0) { - const modifyStroke = !modifiers.ctrlKey && !modifiers.metaKey; - const modifyBackground = !modifiers.shiftKey && (!modifiers.ctrlKey || !modifiers.metaKey); - - for (const el of svgImageElements) { - // Get current color mapping - const colorInfo = await ea.getColorMapForImgElement(el); - const newColorMap = {}; - let hasChanges = false; - - // Process each color in the SVG - for (const [color, info] of colorInfo.entries()) { - let shouldModify = (modifyBackground && info.fill) || (modifyStroke && info.stroke); - - if (shouldModify) { - const modifiedColor = modifyColor(info.mappedTo, modifiers); - if (modifiedColor !== color) { - newColorMap[color] = modifiedColor; - hasChanges = true; + // Process SVG image elements + if (svgImageElements.length > 0) { + for (const el of svgImageElements) { + // Get current color mapping + const colorInfo = await ea.getColorMapForImgElement(el); + const newColorMap = {}; + let hasChanges = false; + + // Process each color in the SVG + for (const [color, info] of colorInfo.entries()) { + let shouldModify = (modifyBackground && info.fill) || (modifyStroke && info.stroke); + + if (shouldModify) { + const modifiedColor = modifyColor(info.mappedTo, isDecrease, step, action); + if (modifiedColor !== color) { + newColorMap[color] = modifiedColor; + hasChanges = true; + } + } + } + + // Update the SVG if any colors were modified + if (hasChanges) { + ea.updateViewSVGImageColorMap(el, newColorMap); } } } - - // Update the SVG if any colors were modified - if (hasChanges) { - ea.updateViewSVGImageColorMap(el, newColorMap); - } + } catch (e) { + new Notice("Error in executeChange. See developer console for details"); + console.error("Error in executeChange:", e); + } finally { + isRunning = false; } -} \ No newline at end of file +} + +showModal(); +processQueue(); \ No newline at end of file