This commit is contained in:
zsviczian
2024-12-27 21:04:33 +01:00
parent 982958a4c6
commit 5a58d17d99
11 changed files with 297 additions and 186 deletions

View File

@@ -9,7 +9,7 @@ Select some elements in the scene. The script will take these elements and move
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.25")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.7.3")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
@@ -81,6 +81,7 @@ ea.getElements().filter(el=>el.type==="image").forEach(el=>{
const path = (img?.linkParts?.original)??(img?.file?.path);
const hyperlink = img?.hyperlink;
if(img && (path || hyperlink)) {
const colorMap = ea.getColorMapForImageElement(el);
ea.imagesDict[el.fileId] = {
mimeType: img.mimeType,
id: el.fileId,
@@ -90,6 +91,7 @@ ea.getElements().filter(el=>el.type==="image").forEach(el=>{
hyperlink,
hasSVGwithBitmap: img.isSVGwithBitmap,
latex: null,
colorMap,
};
return;
}

View File

@@ -10,17 +10,25 @@ If you select only a single SVG or nested Excalidraw element, then the script of
```js*/
const HELP_TEXT = `
- Select SVG images, nested Excalidraw drawings and/or regular Excalidraw elements
- For a single selected image, you can map colors individually in the color mapping section
- For Excalidraw elements: stroke and background colors are modified permanently
- For SVG/nested drawings: original files stay unchanged, color mapping is stored under \`## Embedded Files\`
- Using color maps helps maintain links between drawings while allowing different color themes
- Sliders work on relative scale - the amount of change is applied to current values
- Unlike Excalidraw's opacity setting which affects the whole element:
- Shade Master can set different opacity for stroke vs background
- Note: SVG/nested drawing colors are mapped at color name level, thus "black" is different from "#000000"
- Additionally if the same color is used as fill and stroke the color can only be changed once
- This is an experimental script - contributions welcome on GitHub via PRs
<ul>
<li dir="auto">Select SVG images, nested Excalidraw drawings and/or regular Excalidraw elements</li>
<li dir="auto">For a single selected image, you can map colors individually in the color mapping section</li>
<li dir="auto">For Excalidraw elements: stroke and background colors are modified permanently</li>
<li dir="auto">For SVG/nested drawings: original files stay unchanged, color mapping is stored under <code>## Embedded Files</code></li>
<li dir="auto">Using color maps helps maintain links between drawings while allowing different color themes</li>
<li dir="auto">Sliders work on relative scale - the amount of change is applied to current values</li>
<li dir="auto">Unlike Excalidraw's opacity setting which affects the whole element:
<ul>
<li dir="auto">Shade Master can set different opacity for stroke vs background</li>
<li dir="auto">Note: SVG/nested drawing colors are mapped at color name level, thus "black" is different from "#000000"</li>
<li dir="auto">Additionally if the same color is used as fill and stroke the color can only be mapped once</li>
</ul>
</li>
<li dir="auto">This is an experimental script - contributions welcome on GitHub via PRs</li>
</ul>
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/ISuORbVKyhQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
`;
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.7.3")) {
@@ -46,6 +54,7 @@ interface ColorMap {
// Main script execution
const allElements = ea.getViewSelectedElements();
const svgImageElements = allElements.filter(el => {
if(el.type !== "image") return false;
const file = ea.getViewFileForImageElement(el);
if(!file) return false;
return el.type === "image" && (
@@ -60,6 +69,7 @@ if(allElements.length === 0) {
}
const originalColors = new Map();
const currentColors = new Map();
const colorInputs = new Map();
const sliderResetters = [];
let terminate = false;
@@ -67,6 +77,10 @@ const FORMAT = "Color Format";
const STROKE = "Modify Stroke Color";
const BACKGROUND = "Modify Background Color"
const ACTIONS = ["Hue", "Lightness", "Saturation", "Transparency"];
const precision = [1,2,2,3];
const minLigtness = 1/Math.pow(10,precision[2]);
const maxLightness = 100 - minLigtness;
const minSaturation = 1/Math.pow(10,precision[2]);
let settings = ea.getScriptSettings();
//set default values on first run
@@ -126,11 +140,23 @@ async function storeOriginalColors() {
for (const [color, info] of colorInfo.entries()) {
svgColors.set(color, {...info});
}
const svgColorData = {
type: "svg",
colors: svgColors
};
originalColors.set(el.id, svgColorData);
originalColors.set(el.id, {type: "svg",colors: svgColors});
}
copyOriginalsToCurrent();
}
function copyOriginalsToCurrent() {
for (const [key, value] of originalColors.entries()) {
if(value.type === "regular") {
currentColors.set(key, {...value});
} else {
const newColorMap = new Map();
for (const [color, info] of value.colors.entries()) {
newColorMap.set(color, {...info});
}
currentColors.set(key, {type: "svg", colors: newColorMap});
}
}
}
@@ -142,45 +168,36 @@ function clearSVGMapping() {
if (svgImageElements.length === 1) {
const el = svgImageElements[0];
const original = originalColors.get(el.id);
const current = currentColors.get(el.id);
if (original && original.type === "svg") {
for (const color of original.colors.keys()) {
// Update UI components
const inputs = colorInputs.get(color);
if (inputs) {
if(color === "fill") {
//stroke is a special value in case the SVG has no fill color defined (i.e black)
inputs.textInput.setValue("black");
inputs.colorPicker.setValue("#000000");
} else {
const cm = ea.getCM(color);
inputs.textInput.setValue(color);
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
}
current.colors.get(color).mappedTo = color;
}
updatedImageElementColorMaps.set(el, {});
}
} else {
for (const el of svgImageElements) {
updatedImageElementColorMaps.set(el, {});
const original = originalColors.get(el.id);
const current = currentColors.get(el.id);
if (original && original.type === "svg") {
for (const color of original.colors.keys()) {
current.colors.get(color).mappedTo = color;
}
}
}
}
updateViewImageColors();
run("clear");
}
// Function to reset colors
async function resetColors() {
for (const resetter of sliderResetters) {
resetter();
}
// Set colors
async function setColors(colors) {
debounceColorPicker = true;
const regularElements = getRegularElements();
if (regularElements.length > 0) {
ea.copyViewElementsToEAforEditing(regularElements);
for (const el of ea.getElements()) {
const original = originalColors.get(el.id);
const original = colors.get(el.id);
if (original && original.type === "regular") {
if (original.strokeColor) el.strokeColor = original.strokeColor;
if (original.backgroundColor) el.backgroundColor = original.backgroundColor;
@@ -192,7 +209,7 @@ async function resetColors() {
// Reset SVG elements
if (svgImageElements.length === 1) {
const el = svgImageElements[0];
const original = originalColors.get(el.id);
const original = colors.get(el.id);
if (original && original.type === "svg") {
const newColorMap = {};
@@ -201,16 +218,23 @@ async function resetColors() {
// Update UI components
const inputs = colorInputs.get(color);
if (inputs) {
const cm = ea.getCM(info.mappedTo);
inputs.textInput.setValue(info.mappedTo);
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
if(info.mappedTo === "fill") {
info.mappedTo = "black";
//"fill" is a special value in case the SVG has no fill color defined (i.e black)
inputs.textInput.setValue("black");
inputs.colorPicker.setValue("#000000");
} else {
const cm = ea.getCM(info.mappedTo);
inputs.textInput.setValue(info.mappedTo);
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
}
}
updatedImageElementColorMaps.set(el, newColorMap);
}
} else {
for (const el of svgImageElements) {
const original = originalColors.get(el.id);
const original = colors.get(el.id);
if (original && original.type === "svg") {
const newColorMap = {};
@@ -231,10 +255,20 @@ function modifyColor(color, isDecrease, step, action) {
if (!cm) return color;
let modified = cm;
if (modified.lightness === 0) modified = modified.lightnessTo(minLigtness);
if (modified.lightness === 100) modified = modified.lightnessTo(maxLightness);
if (modified.saturation === 0) modified = modified.saturationTo(minSaturation);
switch(action) {
case "Lightness":
modified = isDecrease ? modified.darkerBy(step) : modified.lighterBy(step);
// handles edge cases where lightness is 0 or 100 would convert saturation and hue to 0
let lightness = cm.lightness;
const shouldRoundLight = (lightness === minLigtness || lightness === maxLightness);
if (shouldRoundLight) lightness = Math.round(lightness);
lightness += isDecrease ? -step : step;
if (lightness <= 0) lightness = minLigtness;
if (lightness >= 100) lightness = maxLightness;
modified = modified.lightnessTo(lightness);
break;
case "Hue":
modified = isDecrease ? modified.hueBy(-step) : modified.hueBy(step);
@@ -243,11 +277,16 @@ function modifyColor(color, isDecrease, step, action) {
modified = isDecrease ? modified.alphaBy(-step) : modified.alphaBy(step);
break;
default:
modified = isDecrease ? modified.desaturateBy(step) : modified.saturateBy(step);
let saturation = cm.saturation;
const shouldRoundSat = saturation === minSaturation;
if (shouldRoundSat) saturation = Math.round(saturation);
saturation += isDecrease ? -step : step;
if (saturation <= 0) saturation = minSaturation;
modified = modified.saturationTo(saturation);
}
const hasAlpha = modified.alpha < 1;
const opts = { alpha: hasAlpha, precision: [1,2,2,3] };
const opts = { alpha: hasAlpha, precision };
const format = settings[FORMAT].value;
switch(format) {
@@ -274,7 +313,7 @@ function slider(contentEl, action, min, max, step, invert) {
const step = Math.abs(value-prevValue);
prevValue = value;
if(step>0) {
run(isDecrease, step, action);
run(action, isDecrease, step);
}
});
}
@@ -300,12 +339,12 @@ function showModal() {
const helpDiv = contentEl.createEl("details", {
attr: { style: "margin-bottom: 1em;background: var(--background-secondary); padding: 1em; border-radius: 4px;" }});
helpDiv.createEl("summary", { text: "Help & Usage Guide", attr: { style: "cursor: pointer; color: var(--text-accent);" } });
helpDiv.createEl("div", {
text: HELP_TEXT,
attr: { style: "margin-top: 0.5em; white-space: pre-wrap;" }
const helpDetailsDiv = helpDiv.createEl("div", {
attr: { style: "margin-top: 0em; " }
});
helpDetailsDiv.innerHTML = HELP_TEXT;
new ea.obsidian.Setting(contentEl)
const component = new ea.obsidian.Setting(contentEl)
.setName(FORMAT)
.setDesc("Output color format")
.addDropdown(dropdown => dropdown
@@ -317,6 +356,7 @@ function showModal() {
.setValue(settings[FORMAT].value)
.onChange(value => {
settings[FORMAT].value = value;
run();
dirty = true;
})
);
@@ -341,9 +381,17 @@ function showModal() {
})
);
sliderResetters.push(slider(contentEl, "Hue", 0, 400, 1, false));
// lightness and saturation are on a scale of 0%-100%
// Hue is in degrees, 360 for the full circle
// transparency is on a range between 0 and 1 (equivalent to 0%-100%)
// The range for lightness, saturation and transparency are double since
// the input could be at either end of the scale
// The range for Hue is 360 since regarless of the position on the circle moving
// the slider to the two extremes will travel the entire circle
// To modify blacks and whites, lightness first needs to be changed to value between 1% and 99%
sliderResetters.push(slider(contentEl, "Hue", 0, 360, 1, false));
sliderResetters.push(slider(contentEl, "Saturation", 0, 200, 1, false));
sliderResetters.push(slider(contentEl, "Lightness", 0, 100, 1, false));
sliderResetters.push(slider(contentEl, "Lightness", 0, 200, 1, false));
sliderResetters.push(slider(contentEl, "Transparency", 0, 2, 0.05, true));
// Add color pickers if a single SVG image is selected
@@ -358,10 +406,12 @@ function showModal() {
const row = new ea.obsidian.Setting(colorSection)
.setName(color === "fill" ? "SVG default" : color)
.setDesc(`${info.fill ? "Fill" : ""}${info.fill && info.stroke ? " & " : ""}${info.stroke ? "Stroke" : ""}`);
row.descEl.style.width = "100px";
row.nameEl.style.width = "100px";
// Create color preview div
const previewDiv = row.controlEl.createDiv();
previewDiv.style.width = "30px";
previewDiv.style.width = "50px";
previewDiv.style.height = "20px";
previewDiv.style.border = "1px solid var(--background-modifier-border)";
if (color === "transparent") {
@@ -378,22 +428,13 @@ function showModal() {
.setClass("reset-color-button")
.onClick(async () => {
const original = originalColors.get(svgElement.id);
const current = currentColors.get(svgElement.id);
if (original?.type === "svg") {
const originalInfo = original.colors.get(color);
const currentInfo = current.colors.get(color);
if (originalInfo) {
const newColorMap = await ea.getColorMapForImageElement(svgElement);
delete newColorMap[color];
updatedImageElementColorMaps.set(svgElement, newColorMap);
updateViewImageColors();
// Update UI components
debounceColorPicker = true;
textInput.setValue(color === "fill" ? "black" : color);
colorPicker.setValue(color === "fill"
? "#000000"
: ea.getCM(color).stringHEX({alpha: false}).toLowerCase()
);
updateViewImageColors();
currentInfo.mappedTo = color;
run("reset single color");
}
}
}))
@@ -404,7 +445,7 @@ function showModal() {
const textInput = new ea.obsidian.TextComponent(row.controlEl)
.setValue(info.mappedTo)
.setPlaceholder("Color value");
textInput.inputEl.style.width = "120px";
textInput.inputEl.style.width = "100%";
textInput.onChange(value => {
const lower = value.toLowerCase();
if (lower === color) return;
@@ -427,19 +468,15 @@ function showModal() {
const format = settings[FORMAT].value;
const alpha = cm.alpha < 1 ? true : false;
const newColor = format === "RGB"
? cm.stringRGB({alpha , precision: [1,2,2,3]}).toLowerCase()
? cm.stringRGB({alpha , precision }).toLowerCase()
: format === "HEX"
? cm.stringHEX({alpha}).toLowerCase()
: cm.stringHSL({alpha, precision: [1,2,2,3]}).toLowerCase();
const newColorMap = await ea.getColorMapForImageElement(svgElement);
if(color === newColor) {
delete newColorMap[color];
} else {
newColorMap[color] = newColor;
}
updatedImageElementColorMaps.set(svgElement, newColorMap);
updateViewImageColors();
: cm.stringHSL({alpha, precision }).toLowerCase();
textInput.setValue(newColor);
const colorInfo = currentColors.get(svgElement.id).colors;
colorInfo.get(color).mappedTo = newColor;
run("no action");
debounceColorPicker = true;
colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
@@ -476,10 +513,10 @@ function showModal() {
const alpha = originalAlpha < 1 ? true : false;
const format = settings[FORMAT].value;
const newColor = format === "RGB"
? cm.stringRGB({alpha, precision: [1,2,2,3]}).toLowerCase()
? cm.stringRGB({alpha, precision }).toLowerCase()
: format === "HEX"
? cm.stringHEX({alpha}).toLowerCase()
: cm.stringHSL({alpha, precision: [1,2,2,3]}).toLowerCase();
: cm.stringHSL({alpha, precision }).toLowerCase();
// Update text input
textInput.setValue(newColor);
@@ -518,8 +555,11 @@ function showModal() {
.addButton(button => button
.setButtonText("Reset")
.onClick(() => {
debounceColorPicker = true;
resetColors();
for (const resetter of sliderResetters) {
resetter();
}
copyOriginalsToCurrent();
setColors(originalColors);
}))
.addButton(button => button
.setButtonText("Close")
@@ -596,101 +636,86 @@ function makeModalDraggable(modalEl) {
});
}
async function executeChange(isDecrease, step, action) {
try {
isRunning = true;
const modifyStroke = settings[STROKE].value;
const modifyBackground = settings[BACKGROUND].value;
const regularElements = getRegularElements();
function executeChange(isDecrease, step, action) {
const modifyStroke = settings[STROKE].value;
const modifyBackground = settings[BACKGROUND].value;
const regularElements = getRegularElements();
// 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);
}
// Process regular elements
if (regularElements.length > 0) {
for (const el of regularElements) {
const currentColor = currentColors.get(el.id);
if (modifyStroke && currentColor.strokeColor) {
currentColor.strokeColor = modifyColor(el.strokeColor, isDecrease, step, action);
}
await ea.addElementsToView(false, false);
}
// Process SVG image elements
if (svgImageElements.length === 1) { // Only update UI for single SVG
const el = svgImageElements[0];
const colorInfo = await ea.getSVGColorInfoForImgElement(el);
const newColorMap = {};
// 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;
}
// Update UI components if they exist
const inputs = colorInputs.get(color);
if (inputs) {
const cm = ea.getCM(modifiedColor);
inputs.textInput.setValue(modifiedColor);
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
}
}
updatedImageElementColorMaps.set(el, newColorMap);
} else {
if (svgImageElements.length > 0) {
for (const el of svgImageElements) {
const colorInfo = await ea.getSVGColorInfoForImgElement(el);
const newColorMap = {};
// 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;
}
}
}
updatedImageElementColorMaps.set(el, newColorMap);
if (modifyBackground && currentColor.backgroundColor) {
currentColor.backgroundColor = modifyColor(el.backgroundColor, isDecrease, step, action);
}
}
}
// Process SVG image elements
if (svgImageElements.length === 1) { // Only update UI for single SVG
const el = svgImageElements[0];
colorInfo = currentColors.get(el.id).colors;
// 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);
colorInfo.get(color).mappedTo = modifiedColor;
// Update UI components if they exist
const inputs = colorInputs.get(color);
if (inputs) {
const cm = ea.getCM(modifiedColor);
inputs.textInput.setValue(modifiedColor);
inputs.colorPicker.setValue(cm.stringHEX({alpha: false}).toLowerCase());
}
}
}
} else {
if (svgImageElements.length > 0) {
for (const el of svgImageElements) {
const colorInfo = currentColors.get(el.id).colors;
// 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);
colorInfo.get(color).mappedTo = modifiedColor;
}
}
}
}
} catch (e) {
new Notice("Error in executeChange. See developer console for details");
console.error("Error in executeChange:", e);
} finally {
isRunning = false;
}
}
isRunning = false;
const queue = [];
let isRunning = false;
let queue = false;
function processQueue() {
if (!terminate && !isRunning && queue.length > 0) {
const [isDecrease, step, action] = queue.shift();
executeChange(isDecrease, step, action).then(() => {
updateViewImageColors()
if (queue.length > 0) processQueue();
if (!terminate && !isRunning && queue) {
queue = false;
isRunning = true;
setColors(currentColors).then(() => {
isRunning = false;
if (queue) processQueue();
});
}
}
const MAX_QUEUE_SIZE = 100;
function run(isDecrease, step, action) {
if (queue.length >= MAX_QUEUE_SIZE) {
new Notice ("Queue overflow. Dropping task.");
return;
function run(action="Hue", isDecrease=true, step=0) {
// passing invalid action (such as "clear") will bypass rewriting of colors using CM
// this is useful when resetting colors to original values
if(ACTIONS.includes(action)) {
executeChange(isDecrease, step, action);
}
queue.push([isDecrease, step, action]);
queue = true;
if (!isRunning) processQueue();
}

File diff suppressed because one or more lines are too long

View File

@@ -568,7 +568,7 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Shade%20Master.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Shade%20Master.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">You can modify the colors of SVG images, embedded files, and Excalidraw elements in a drawing by changing Hue, Saturation, Lightness and Transparency; and if only a single SVG or nested Excalidraw drawing is selected, then you can remap image colors.</td></tr></table>
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Shade%20Master.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">You can modify the colors of SVG images, embedded files, and Excalidraw elements in a drawing by changing Hue, Saturation, Lightness and Transparency; and if only a single SVG or nested Excalidraw drawing is selected, then you can remap image colors.<br><iframe width="560" height="315" src="https://www.youtube.com/embed/ISuORbVKyhQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></td></tr></table>
## Slideshow

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.7.2",
"version": "2.7.3",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.7.2",
"version": "2.7.3",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -18,16 +18,20 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://storage.ko-fi.com/cdn/kofi6.png?v=6" border="0" alt="Buy Me a Coffee at ko-fi.com" height=45></a></div>
`,
"2.7.3":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/ISuORbVKyhQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Fixed
- Toggling image size anchoring on and off by modifying the image link did not update the image in the view until the user forced saved it or closed and opened the drawing again. This was a side-effect of the less frequent view save introduced in 2.7.1
## New
- **Shade Master Script**: A new script that allows you to modify the color lightness, hue, saturation, and transparency of selected Excalidraw elements, SVG images, and nested Excalidraw drawings. When a single image is selected, you can map colors individually. The original image remains unchanged, and a mapping table is added under ${String.fromCharCode(96)}## Embedded Files${String.fromCharCode(96)} for SVG and nested drawings. This helps maintain links between drawings while allowing different color themes.
- New Command Palette Command: "Duplicate selected image with a different image ID". Creates a copy of the selected image with a new image ID. This allows you to add multiple color mappings to the same image. In the scene the image will be treated as if a different image, but loaded from the same file in the Vault.
- New Command Palette Command: "Duplicate selected image with a different image ID". Creates a copy of the selected image with a new image ID. This allows you to add multiple color mappings to the same image. In the scene, the image will be treated as if a different image, but loaded from the same file in the Vault.
## QoL Improvements
- New setting under ${String.fromCharCode(96)}Embedding Excalidraw into your notes and Exporting${String.fromCharCode(96)} > ${String.fromCharCode(96)}Image Caching and rendering optimization${String.fromCharCode(96)}. You can now set the number of concurrent workers that render your embedded images. Increasing the number will increase the speed but temporarily reduce the responsiveness of your system in case of large drawings.
- Moved pen-related settings under ${String.fromCharCode(96)}Excalidraw appearance and behavior${String.fromCharCode(96)} to their own sub-heading called ${String.fromCharCode(96)}Pen${String.fromCharCode(96)}.
- Moved pen-related settings under ${String.fromCharCode(96)}Excalidraw appearance and behavior${String.fromCharCode(96)} to their sub-heading called ${String.fromCharCode(96)}Pen${String.fromCharCode(96)}.
- Minor error fixing and performance optimizations when loading and updating embedded images.
- Color maps in ${String.fromCharCode(96)}## Embedded Files${String.fromCharCode(96)} may now include color keys "stroke" and "fill". If set, these will change the fill and stroke attributes of the SVG root element of the relevant file.
@@ -57,6 +61,21 @@ getColosFromExcalidrawFile(file:TFile, img: ExcalidrawImageElement): Promise<SVG
// Extracts the fill and stroke colors from an SVG string and returns them as an SVGColorInfo.
getColorsFromSVGString(svgString: string): SVGColorInfo;
// upgraded the addImage function.
// 1. It now accepts an object as the input parameter, making your scripts more readable
// 2. AddImageOptions now includes colorMap as an optional parameter, this will only have an effect in case of SVGs and nested Excalidraws
// 3. The API function is backwards compatible, but I recommend new implementations to use the object based input
addImage(opts: AddImageOptions}): Promise<string>;
interface AddImageOptions {
topX: number;
topY: number;
imageFile: TFile | string;
scale?: boolean;
anchor?: boolean;
colorMap?: ColorMap;
}
type SVGColorInfo = Map<string, {
mappedTo: string;
fill: boolean;

View File

@@ -297,8 +297,13 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "addImage",
code: "async addImage(topX: number, topY: number, imageFile: TFile|string, scale?: boolean, anchor?: boolean): Promise<string>;",
desc: "imageFile may be a TFile or a string that contains a hyperlink. imageFile may also be an obsidian filepath including a reference eg.: 'path/my.pdf#page=3'\nSet scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image.\nanchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened.",
code: "async addImage(opts: {topX: number, topY: number, imageFile: TFile|string, scale?: boolean, anchor?: boolean, colorMap?: ColorMap}): Promise<string>;",
desc: "imageFile may be a TFile or a string that contains a hyperlink.\n"+
"imageFile may also be an obsidian filepath including a reference eg.: 'path/my.pdf#page=3'\n"+
"Set scale to false if you want to embed the image at 100% of its original size. Default is true which will insert a scaled image.\n"+
"anchor will only be evaluated if scale is false. anchor true will add |100% to the end of the filename, resulting in an image that will always pop back to 100% when the source file is updated or when the Excalidraw file is reopened.\n"+
"colorMap is only used for SVG images and nested Excalidraw images. See the Shade Master script and the Deconstruct Selected Elements script for examples using colorMap.\n"+
"type ColorMap = { [color: string]: string; }",
after: "",
},
{

View File

@@ -95,8 +95,9 @@ import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "../utils/exc
import { log } from "../utils/debugHelper";
import { ExcalidrawLib } from "../types/excalidrawLib";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types";
import { SVGColorInfo } from "src/types/excalidrawAutomateTypes";
import { AddImageOptions, ImageInfo, SVGColorInfo } from "src/types/excalidrawAutomateTypes";
import { errorMessage, filterColorMap, getEmbeddedFileForImageElment, isColorStringTransparent, isSVGColorInfo, mergeColorMapIntoSVGColorInfo, svgColorInfoToColorMap, updateOrAddSVGColorInfo } from "src/utils/excalidrawAutomateUtils";
import { Color } from "chroma-js";
extendPlugins([
HarmonyPlugin,
@@ -392,7 +393,7 @@ export class ExcalidrawAutomate {
plugin: ExcalidrawPlugin;
elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId
imagesDict: {[key: FileId]: ImageInfo}; //the images files including DataURL, indexed by fileId
mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes
style: {
strokeColor: string; //https://www.w3schools.com/colors/default.asp
@@ -774,6 +775,10 @@ export class ExcalidrawAutomate {
? `\n## Embedded Files\n`
: "\n";
const embeddedFile = (key: FileId, path: string, colorMap?:ColorMap): string => {
return `${key}: [[${path}]]${colorMap ? " " + JSON.stringify(colorMap): ""}\n\n`;
}
Object.keys(this.imagesDict).forEach((key: FileId)=> {
const item = this.imagesDict[key];
if(item.latex) {
@@ -781,9 +786,9 @@ export class ExcalidrawAutomate {
} else {
if(item.file) {
if(item.file instanceof TFile) {
outString += `${key}: [[${item.file.path}]]\n\n`;
outString += embeddedFile(key,item.file.path, item.colorMap);
} else {
outString += `${key}: [[${item.file}]]\n\n`;
outString += embeddedFile(key,item.file, item.colorMap);
}
} else {
const hyperlinkSplit = item.hyperlink.split("#");
@@ -791,8 +796,8 @@ export class ExcalidrawAutomate {
if(file && file instanceof TFile) {
const hasFileRef = hyperlinkSplit.length === 2
outString += hasFileRef
? `${key}: [[${file.path}#${hyperlinkSplit[1]}]]\n\n`
: `${key}: [[${file.path}]]\n\n`;
? embeddedFile(key,`${file.path}#${hyperlinkSplit[1]}`, item.colorMap)
: embeddedFile(key,file.path, item.colorMap);
} else {
outString += `${key}: ${item.hyperlink}\n\n`;
}
@@ -1537,12 +1542,26 @@ export class ExcalidrawAutomate {
* @returns
*/
async addImage(
topX: number,
topXOrOpts: number | AddImageOptions,
topY: number,
imageFile: TFile | string, //string may also be an Obsidian filepath with a reference such as folder/path/my.pdf#page=2
scale: boolean = true, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
anchor: boolean = true, //only has effect if scale is false. If anchor is true the image path will include |100%, if false the image will be inserted at 100%, but if resized by the user it won't pop back to 100% the next time Excalidraw is opened.
): Promise<string> {
let colorMap: ColorMap;
let topX: number;
if(typeof topXOrOpts === "number") {
topX = topXOrOpts;
} else {
topY = topXOrOpts.topY;
topX = topXOrOpts.topX;
imageFile = topXOrOpts.imageFile;
scale = topXOrOpts.scale ?? true;
anchor = topXOrOpts.anchor ?? true;
colorMap = topXOrOpts.colorMap;
}
const id = nanoid();
const loader = new EmbeddedFilesLoader(
this.plugin,
@@ -1576,6 +1595,7 @@ export class ExcalidrawAutomate {
height: image.size.height,
width: image.size.width,
},
colorMap,
};
if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) {
const scale =
@@ -2031,6 +2051,13 @@ export class ExcalidrawAutomate {
ef.colorMap = null;
} else {
ef.colorMap = colorMap;
//delete special mappings for default/SVG root color values
if (ef.colorMap["fill"] === "black") {
delete ef.colorMap["fill"];
}
if (ef.colorMap["stroke"] === "none") {
delete ef.colorMap["stroke"];
}
}
ef.resetImage(this.targetView.file.path, ef.linkParts.original);
fileIDWhiteList.add(el.fileId);
@@ -2194,21 +2221,20 @@ export class ExcalidrawAutomate {
id: el.fileId,
dataURL: sceneFile.dataURL,
created: sceneFile.created,
hasSVGwithBitmap: ef ? ef.isSVGwithBitmap : false,
...ef ? {
isHyperLink: ef.isHyperLink || imageWithRef,
isHyperLink: ef.isHyperLink || Boolean(imageWithRef),
hyperlink: imageWithRef ? `${ef.file.path}#${ef.linkParts.ref}` : ef.hyperlink,
file: imageWithRef ? null : ef.file,
hasSVGwithBitmap: ef.isSVGwithBitmap,
latex: null,
} : {},
...equation ? {
file: null,
isHyperLink: false,
hyperlink: null,
hasSVGwithBitmap: false,
latex: equation.latex,
} : {},
};
}
}
});
} else {

View File

@@ -253,6 +253,12 @@ export class ScriptEngine {
if (!view || !script || !title) {
return;
}
//addresses the situation when after paste text element IDs are not updated to 8 characters
//linked to onPaste save issue with the false parameter
if(view.getScene().elements.some(el=>!el.isDeleted && el.type === "text" && el.id.length > 8)) {
await view.save(false, true);
}
script = script.replace(/^---.*?---\n/gs, "");
const ea = getEA(view);
this.eaInstances.push(ea);

View File

@@ -1,5 +1,33 @@
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { TFile } from "obsidian";
import { FileId } from "src/core";
import { ColorMap, MimeType } from "src/shared/EmbeddedFileLoader";
export type SVGColorInfo = Map<string, {
mappedTo: string;
fill: boolean;
stroke: boolean;
}>;
}>;
export type ImageInfo = {
mimeType: MimeType,
id: FileId,
dataURL: DataURL,
created: number,
isHyperLink?: boolean,
hyperlink?: string,
file?:string | TFile,
hasSVGwithBitmap: boolean,
latex?: string,
size?: {height: number, width: number},
colorMap?: ColorMap,
}
export interface AddImageOptions {
topX: number;
topY: number;
imageFile: TFile | string;
scale?: boolean;
anchor?: boolean;
colorMap?: ColorMap;
}