mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
8.2 KiB
8.2 KiB
/*
The script will convert your drawing into a slideshow presentation.
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.8.2")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
//constants
const STEPCOUNT = 100;
const FRAME_SLEEP = 1; //milliseconds
//utility & convenience functions
const doc = ea.targetView.ownerDocument;
const win = ea.targetView.ownerWindow;
const api = ea.getExcalidrawAPI();
const contentEl = ea.targetView.contentEl;
const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));
//clean up potential clutter from previous run
window.removePresentationEventHandlers?.();
//check if line or arrow is selected, if not inform the user and terminate presentation
const lineEl = ea.getViewSelectedElement();
if(!lineEl || !["line","arrow"].contains(lineEl.type)) {
new Notice("Please select the line or arrow for the presentation path");
return;
}
//goto fullscreen
if(app.isMobile) {
ea.viewToggleFullScreen(true);
} else {
await contentEl.requestFullscreen();
await sleep(50);
ea.setViewModeEnabled(true);
}
const deltaWidth = () => contentEl.clientWidth-api.getAppState().width;
let watchdog = 0;
while (deltaWidth()>50 && watchdog++<20) await sleep(100); //wait for Excalidraw to resize to fullscreen
contentEl.querySelector(".layer-ui__wrapper").addClass("excalidraw-hidden");
//hide the arrow and save the arrow color before doing so
const originalColor = {
strokeColor: lineEl.strokeColor,
backgroundColor: lineEl.backgroundColor
}
ea.copyViewElementsToEAforEditing([lineEl]);
ea.getElement(lineEl.id).strokeColor = "transparent";
ea.getElement(lineEl.id).backgroundColor = "transparent";
await ea.addElementsToView();
//----------------------------
//scroll-to-location functions
//----------------------------
let slide = -1;
const slideCount = Math.floor(lineEl.points.length/2)-1;
const getNextSlide = (forward) => {
slide = forward
? slide < slideCount ? slide + 1 : 0
: slide <= 0 ? slideCount : slide - 1;
return {pointA:lineEl.points[slide*2], pointB:lineEl.points[slide*2+1]}
}
const getSlideRect = ({pointA, pointB}) => {
const {width, height} = api.getAppState();
const x1 = lineEl.x+pointA[0];
const y1 = lineEl.y+pointA[1];
const x2 = lineEl.x+pointB[0];
const y2 = lineEl.y+pointB[1];
const ratioX = width/Math.abs(x1-x2);
const ratioY = height/Math.abs(y1-y2);
let ratio = ratioX<ratioY?ratioX:ratioY;
if (ratio < 0.1) ratio = 0.1;
if (ratio > 10) ratio = 10;
const deltaX = (ratio===ratioY)?(width/ratio - Math.abs(x1-x2))/2:0;
const deltaY = (ratio===ratioX)?(height/ratio - Math.abs(y1-y2))/2:0;
return {
left: (x1<x2?x1:x2)-deltaX,
top: (y1<y2?y1:y2)-deltaY,
right: (x1<x2?x2:x1)+deltaX,
bottom: (y1<y2?y2:y1)+deltaY,
nextZoom: ratio
};
}
let busy = false;
const scrollToNextRect = async ({left,top,right,bottom,nextZoom}) => {
let watchdog = 0;
while(busy && watchdog++<15) await(100);
if(busy && watchdog >= 15) return;
busy = true;
const {scrollX, scrollY, zoom} = api.getAppState();
const zoomStep = (zoom.value-nextZoom)/STEPCOUNT;
const xStep = (left+scrollX)/STEPCOUNT;
const yStep = (top+scrollY)/STEPCOUNT;
for(i=1;i<=STEPCOUNT;i++) {
api.updateScene({
appState: {
scrollX:scrollX-(xStep*i),
scrollY:scrollY-(yStep*i),
zoom:{value:zoom.value-zoomStep*i},
shouldCacheIgnoreZoom:true,
}
});
await sleep(FRAME_SLEEP);
}
api.updateScene({appState:{shouldCacheIgnoreZoom:false}});
busy = false;
}
const navigate = async (dir) => {
const forward = dir === "fwd";
const prevSlide = slide;
const nextSlide = getNextSlide(forward);
//exit if user navigates from last slide forward or first slide backward
const shouldExit = forward
? slide<=prevSlide
: slide>=prevSlide;
if(shouldExit) {
if(!app.isMobile) await doc.exitFullscreen();
exitPresentation();
return;
}
if(slideNumberEl) slideNumberEl.innerText = (slide+1).toString();
const nextRect = getSlideRect(nextSlide);
await scrollToNextRect(nextRect);
}
//--------------------------------------
//Slideshow control
//--------------------------------------
//create slideshow controlpanel container
const top = contentEl.innerHeight;
const left = contentEl.innerWidth;
const containerEl = contentEl.createDiv({
cls: ["excalidraw","excalidraw-presentation-panel"],
attr: {
style: `
width: calc(var(--default-button-size)*3);
z-index:5;
position: absolute;
top:calc(${top}px - var(--default-button-size)*2);
left:calc(${left}px - var(--default-button-size)*3.5);`
}
});
const panelColumn = containerEl.createDiv({
cls: "panelColumn",
});
let slideNumberEl;
panelColumn.createDiv({
cls: ["Island", "buttonList"],
attr: {
style: `
height: calc(var(--default-button-size)*1.5);
width: 100%;
background: var(--island-bg-color);`,
}
}, el=>{
el.createEl("button",{
text: "<",
attr: {
style: `
margin-top: calc(var(--default-button-size)*0.25);
margin-left: calc(var(--default-button-size)*0.25);`
}
}, button => button .onclick = () => navigate("bkwd"));
el.createEl("button",{
text: ">",
attr: {
style: `
margin-top: calc(var(--default-button-size)*0.25);
margin-right: calc(var(--default-button-size)*0.25);`
}
}, button => button.onclick = () => navigate("fwd"));
slideNumberEl = el.createEl("span",{
text: "1",
cls: ["ToolIcon__keybinding"],
})
});
//keyboard navigation
const keydownListener = (e) => {
e.preventDefault();
switch(e.key) {
case "escape":
if(app.isMobile) exitPresentation();
break;
case "ArrowRight":
case "ArrowDown":
navigate("fwd");
break;
case "ArrowLeft":
case "ArrowUp":
navigate("bkwd");
break;
}
}
doc.addEventListener('keydown',keydownListener);
//slideshow panel drag
let pos1 = pos2 = pos3 = pos4 = 0;
const updatePosition = (deltaY = 0, deltaX = 0) => {
const {
offsetTop,
offsetLeft,
clientWidth: width,
clientHeight: height,
} = containerEl;
containerEl.style.top = (offsetTop - deltaY) + 'px';
containerEl.style.left = (offsetLeft - deltaX) + 'px';
}
const pointerUp = () => {
win.removeEventListener('pointermove', onDrag, true);
}
const pointerDown = (e) => {
pos3 = e.clientX;
pos4 = e.clientY;
win.addEventListener('pointermove', onDrag, true);
}
const onDrag = (e) => {
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
updatePosition(pos2, pos1);
}
containerEl.addEventListener('pointerdown', pointerDown, false);
win.addEventListener('pointerup', pointerUp, false);
//event listners for terminating the presentation
window.removePresentationEventHandlers = () => {
ea.onLinkClickHook = null;
containerEl.parentElement?.removeChild(containerEl);
if(!app.isMobile) win.removeEventListener('fullscreenchange', fullscreenListener);
doc.removeEventListener('keydown',keydownListener);
win.removeEventListener('pointerup',pointerUp);
contentEl.querySelector(".layer-ui__wrapper").removeClass("excalidraw-hidden");
delete window.removePresentationEventHandlers;
}
const exitPresentation = () => {
window.removePresentationEventHandlers?.();
if(app.isMobile) ea.viewToggleFullScreen(true);
else ea.setViewModeEnabled(false);
ea.clear();
ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el=>el.id === lineEl.id));
ea.getElement(lineEl.id).strokeColor = originalColor.strokeColor;
ea.getElement(lineEl.id).backgroundColor = originalColor.backgroundColor;
ea.addElementsToView();
ea.selectElementsInView(ea.getElements());
}
ea.onLinkClickHook = () => {
exitPresentation();
return true;
};
const fullscreenListener = (e) => {
e.preventDefault();
exitPresentation();
}
if(!app.isMobile) {
win.addEventListener('fullscreenchange', fullscreenListener);
}
//navigate to the first slide on start
setTimeout(()=>navigate("fwd"));