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.
-
- - If neither of ${modKeys.shift} and ${modKeys.ctrl} are down the script will modify both stroke and background colors
- - ${modKeys.shift}: Modify only the stroke color
- - ${modKeys.ctrl}: Modify only the background color
- - ${modKeys.alt}: Increase/Decrease hue/ligthness/saturation/transparency (based on setting)
- - ${modKeys.shift} and ${modKeys.ctrl}: Click to toggle script action between hue/lightness/saturation/transparency.
- - ${modKeys.shift} and ${modKeys.ctrl}: Double-click to display current action
-
- 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