mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
queue and sliders
This commit is contained in:
@@ -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 = `
|
||||
<h3>Instructions</h3>
|
||||
<p>The script button will perform a range of actions depending on how you click and which modifier buttons you press when clicking.</p>
|
||||
<ul>
|
||||
<li>If neither of <kbd>${modKeys.shift}</kbd> and <kbd>${modKeys.ctrl}</kbd> are down the script will modify both stroke and background colors</li>
|
||||
<li><kbd>${modKeys.shift}</kbd>: Modify only the stroke color</li>
|
||||
<li><kbd>${modKeys.ctrl}</kbd>: Modify only the background color</li>
|
||||
<li><kbd>${modKeys.alt}</kbd>: Increase/Decrease hue/ligthness/saturation/transparency (based on setting)</li>
|
||||
<li><kbd>${modKeys.shift}</kbd> and <kbd>${modKeys.ctrl}</kbd>: Click to toggle script action between hue/lightness/saturation/transparency.</li>
|
||||
<li><kbd>${modKeys.shift}</kbd> and <kbd>${modKeys.ctrl}</kbd>: Double-click to display current action</li>
|
||||
</ul>
|
||||
<h3>Settings</h3>
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showModal();
|
||||
processQueue();
|
||||
Reference in New Issue
Block a user