Skip to content

Instantly share code, notes, and snippets.

@zsviczian
Last active April 12, 2025 15:08
Show Gist options
  • Save zsviczian/33ff695d5b990de1ebe8b82e541c26ad to your computer and use it in GitHub Desktop.
Save zsviczian/33ff695d5b990de1ebe8b82e541c26ad to your computer and use it in GitHub Desktop.
Excalidraw Icon Library

Please check out my YouTube video Create a library of your downloaded icons with Excalidraw Automate in Obsidian, it provides a detailed walkthrough of this code.

To save these files into your Vault, either download them, or switch to RAW mode to copy/paste them into your Obsidian Vault. You may need to use CTRL+SHIFT+V to paste.

Icons created by the script are locked in the scene, you can right click them to select and copy them. Locking the eleemnts avoids the need for switching to view mode and ensures the file does not get accidently modified, thus the images in the image library are not actually saved to the image library causing unwanted links in your Vault.

Advanced usage

I use the script with LOCK_ICONS=true; because in this case I won't accidently move items in the image library causing it to save the icons in the file and creating unwanted links in Excalibrain pointing back to the icon library. If you lock icons then you need to right click the selected icon to copy it to the clipboard, however, when you paste the icon to your drawing it will be locked - which is not convenient. For this reason I've also configured an onPaste event handler via the Excalidraw startup script. This automatically unlocks all elements on paste.

ea.onPasteHook = (data) => {
  if(data?.payload?.elements) {
    data.payload.elements.filter(el=>el.locked).forEach(el=>{el.locked = false;});
  }
};
image

<%* /*

*/
s = await navigator.clipboard.readText();
navigator.clipboard.writeText(s.replaceAll("\n","").replaceAll(/\s{2,}/g," "));
%>

/*

const FILENAME_FILTER = /^icon -/i;
const KEYWORD_GRABBER = /(?:icon -)?([^-]*)-?/i;
const COLS = 30;
const LOCK_ICONS = true;
const HEIGHT = 180;
const WIDTH = 180;
const TEXTHEIGHT = 40;
const PADDING = 50;

const api = ea.getExcalidrawAPI();
const f = ea.targetView.file;

const {
  zenModeEnabled,
  linkOpacity,
  trayModeEnabled,
  penMode,
  penDetected,
  allowPinchZoom,
  allowWheelZoom,
  pinnedScripts,
  customPens,
  zoom
} = api.getAppState();

api.resetScene();
api.updateScene({
  appState: {
    zenModeEnabled,
    linkOpacity,
    trayModeEnabled,
    penMode,
    penDetected,
    allowPinchZoom,
    allowWheelZoom,
    pinnedScripts,
    customPens,
    zoom
  }
});

const promisePool = async (tasks, concurrency, taskHandler) => {
  const results = [];
  const executing = [];

  for (const task of tasks) {
    const p = Promise.resolve().then(() => taskHandler(task));
    results.push(p);

    if (concurrency <= tasks.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e);
      if (executing.length >= concurrency) {
        await Promise.race(executing);
      }
    }
  }
  return Promise.all(results);
};

const processIcon = async (task) => {
  const row = task.row;
  const rowOfIcons = task.icons;
  const eaTemp = ea.getAPI(ea.targetView);
  for (let col=0;col<rowOfIcons.length;col++) {
    const icon = rowOfIcons[col];
    const id = await eaTemp.addImage(col * (WIDTH + PADDING), row * (HEIGHT + PADDING + TEXTHEIGHT), icon);
    if (f !== eaTemp.targetView.file && eaTemp.targetView?.getViewType?.() !== 'excalidraw') continue;
    if (!id) continue;
  
    const keywords = icon.basename.match(KEYWORD_GRABBER)[1].trim();
    eaTemp.style.verticalAlign = 'top';
    eaTemp.style.textAlign = 'center';
    eaTemp.style.fontSize = 12;
  
    const el = eaTemp.getElement(id);
    let ratio = el.width / WIDTH;
    if (el.height / ratio > HEIGHT) ratio = el.height / HEIGHT;
    el.width = el.width / ratio;
    el.height = el.height / ratio;
    el.locked = LOCK_ICONS;
  
    eaTemp.style.strokeColor = 'black';
    const labelID = eaTemp.addText(col * (WIDTH + PADDING) - PADDING / 2 + 10, row * (HEIGHT + PADDING + TEXTHEIGHT) + HEIGHT + PADDING / 2 - 10, keywords, {
      width: WIDTH + PADDING - 20,
      height: TEXTHEIGHT - 20,
      textAlign: 'center',
      textVerticalAlign: 'top',
      autoResize: false,
    });
    eaTemp.getElement(labelID).locked = LOCK_ICONS;
  }

  await eaTemp.addElementsToView(false, false, false);
  eaTemp.targetView.clearDirty();
  eaTemp.destroy();
};

const icons = app.vault.getFiles()
  .filter(f => (f.extension !== 'md' || ea.isExcalidrawFile(f)) && f.basename.toLowerCase().match(FILENAME_FILTER))
  .sort((a, b) => a.basename.toLowerCase() < b.basename.toLowerCase() ? -1 : 1);
const numRows = Math.ceil(icons.length / COLS);
const iconsInRows = [];
for (let i = 0; i < numRows; i++) {
  iconsInRows.push({icons: icons.slice(i * COLS, (i + 1) * COLS), row:i});
};

const concurrency = 3;
await promisePool(iconsInRows, concurrency, processIcon);

ea.targetView.clearDirty();
api.zoomToFit();
api.updateContainerSize(ea.getViewElements().filter(el => el.type === 'rectangle'));
excalidraw-plugin excalidraw-onload-script
parsed
const FILENAME_FILTER = /^icon -/i;const KEYWORD_GRABBER = /(?:icon -)?([^-]*)-?/i;const COLS = 30;const LOCK_ICONS = false;const HEIGHT = 180;const WIDTH = 180;const TEXTHEIGHT = 40;const PADDING = 50;const api = ea.getExcalidrawAPI();const f = ea.targetView.file;const { zenModeEnabled, linkOpacity, trayModeEnabled, penMode, penDetected, allowPinchZoom, allowWheelZoom, pinnedScripts, customPens, zoom} = api.getAppState();api.resetScene();api.updateScene({ appState: { zenModeEnabled, linkOpacity, trayModeEnabled, penMode, penDetected, allowPinchZoom, allowWheelZoom, pinnedScripts, customPens, zoom }});const promisePool = async (tasks, concurrency, taskHandler) => { const results = []; const executing = []; for (const task of tasks) { const p = Promise.resolve().then(() => taskHandler(task)); results.push(p); if (concurrency <= tasks.length) { const e = p.then(() => executing.splice(executing.indexOf(e), 1)); executing.push(e); if (executing.length >= concurrency) { await Promise.race(executing); } } } return Promise.all(results);};const processIcon = async (task) => { const row = task.row; const rowOfIcons = task.icons; const eaTemp = ea.getAPI(ea.targetView); for (let col=0;col<rowOfIcons.length;col++) { const icon = rowOfIcons[col]; const id = await eaTemp.addImage(col * (WIDTH + PADDING), row * (HEIGHT + PADDING + TEXTHEIGHT), icon); if (f !== eaTemp.targetView.file && eaTemp.targetView?.getViewType?.() !== 'excalidraw') continue; if (!id) continue; const keywords = icon.basename.match(KEYWORD_GRABBER)[1].trim(); eaTemp.style.verticalAlign = 'top'; eaTemp.style.textAlign = 'center'; eaTemp.style.fontSize = 12; const el = eaTemp.getElement(id); let ratio = el.width / WIDTH; if (el.height / ratio > HEIGHT) ratio = el.height / HEIGHT; el.width = el.width / ratio; el.height = el.height / ratio; el.locked = LOCK_ICONS; eaTemp.style.strokeColor = 'black'; const labelID = eaTemp.addText(col * (WIDTH + PADDING) - PADDING / 2 + 10, row * (HEIGHT + PADDING + TEXTHEIGHT) + HEIGHT + PADDING / 2 - 10, keywords, { width: WIDTH + PADDING - 20, height: TEXTHEIGHT - 20, textAlign: 'center', textVerticalAlign: 'top', autoResize: false, }); eaTemp.getElement(labelID).locked = LOCK_ICONS; } await eaTemp.addElementsToView(false, false, false); eaTemp.targetView.clearDirty(); eaTemp.destroy();};const icons = app.vault.getFiles() .filter(f => (f.extension !== 'md' || ea.isExcalidrawFile(f)) && f.basename.toLowerCase().match(FILENAME_FILTER)) .sort((a, b) => a.basename.toLowerCase() < b.basename.toLowerCase() ? -1 : 1);const numRows = Math.ceil(icons.length / COLS);const iconsInRows = [];for (let i = 0; i < numRows; i++) { iconsInRows.push({icons: icons.slice(i * COLS, (i + 1) * COLS), row:i});};const concurrency = 3;await promisePool(iconsInRows, concurrency, processIcon);ea.targetView.clearDirty();api.zoomToFit();api.updateContainerSize(ea.getViewElements().filter(el => el.type === 'rectangle'));

#exclude

Excalidraw Data

Text Elements

%%

Drawing

N4KAkARALgngDgUwgLgAQQQDwMYEMA2AlgCYBOuA7hADTgQBuCpAzoQPYB2KqATLZMzYBXUtiRoIACyhQ4zZAHoFAc0JRJQgEYA6bGwC2CgF7N6hbEcK4OCtptbErHALRY8RMpWdx8Q1TdIEfARcZgRmBShcZQUebQBWbR4aOiCEfQQOKGZuAG0AXX4IXDg4AGUoqHFUUDBIdXTqiCJlaRS6hkIECgAhXGwAa2VSYQ5iAGE2fDZSbggAYgAzZZX2

yGwRQKyASSr9CpGBhEnp2Yl5gEYEK6u1iA3SLahd9L7B4dGJqZm5qHIOZhwXBPO4PJ4vfQAMUI+HwFRgwTmgg8oM2mWeewObCOAHUSOpuHxwOs0TtMX9sQh4YiJMiSKjHuiIQAlYStDjhHJoC78EmMsnpADyQOwahg3AuAAZJbz7qSMelIZwoJDcPoYeK0PFZWCmXslVkyoQjNUeDLiXL+Qr9AAVLBQACCLS4EmCiygDPB5OBjsebAokhCxG4HCE

sJ18ohAFExg6/QGg3NgSMqBGrRC4ymbfAmiMhGM7sxsCNYQANbjmjpFkv4ACaFdlRjYBm4tQ69AIQmqF2JAF80170qz88QOcwueg8wXZcMSEaTYTK5BZ8QKgg4NxtRaVwBZNjEBAx3CaYLBtCLAhhGekEgnH5oNuQHpTM/oebYD+ftaQZkIZRh4E5nmBB4hAkCID7Ad0SxI5hSgdgARDMN8FlRZyAyX8xiYQgOGUVtiUgTJj1Pbg/i7Xl1iIDc0D

IhAKIgDg1WqWj6OEKAiA5UjSC7SCLTsAArBBsGyMpGLgPcDyPE8EFfC98CvC1+ngxgbWbfB8LqeocyRNJhIQ785WYKADGzRAkPDAjmjYQYZO4OSFM05pQkdPSVLU8z8D7cB+zoRYYXCVtexAXsgA

%%

@brian-latorre
Copy link

Hi! Any way to automatically set "invert colors"?

@sjr001917
Copy link

I had the same issue as LEEJOOPIL.
Added the script to a new installation with only the bare minimum of community plugins.
The error is "Uncaught SyntaxError: Illegal return statement" and points to this code: if(f!==ea.targetView.file && ea.targetView?.getViewType?.()!=="excalidraw") return;
Removing that line doesn't help either.

@zsviczian
Copy link
Author

This sounds like a plugin is automatically modifying files, e.g. adding some metadata, etc, that creates a syntax error.
Disable other plugins. Delete and re-add the script. Is the error still happening?

Please share the output of command palette "Debug info" so I can see your definition of bare minimum.

@sjr001917
Copy link

Debug Info:
SYSTEM INFO:
Obsidian version: v1.5.12
Installer version: v1.5.3
Operating system: Windows 10 Home 10.0.22631
Login status: not logged in
Insider build toggle: off
Live preview: on
Base theme: dark
Community theme: AnuPpuccin v1.4.5
Snippets enabled: 3
Restricted mode: off
Plugins installed: 1
Plugins enabled: 1
1: Excalidraw v2.2.4

RECOMMENDATIONS:
Custom theme and snippets: for cosmetic issues, please first try updating your theme and disabling your snippets. If still not fixed, please try to make the issue happen in the Sandbox Vault or disable community theme and snippets.
Community plugins: for bugs, please first try updating all your plugins to latest. If still not fixed, please try to make the issue happen in the Sandbox Vault or disable community plugins.

I deleted all plugins except Excalidraw.
Deleted the Icon Library script and Icon Library.md from the vault.
Copied the raw Icon Library script.
Created a new empty drawing
opened the Console
added ea=excalidraw.automate
ea.setview('first')
pasted in the icon library script
and still the same error.
Drawing_2024-06-03_19 25 20_-_Icon-O_-_Obsidian_v12406-05

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment