queue and sliders

This commit is contained in:
zsviczian
2024-12-25 00:31:24 +01:00
parent f384e95e44
commit 1f95f57e97

View File

@@ -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();