/* The script performs two different functions depending on the elements selected in the view. 1) In case you select text elements, the script will cycle through a set of font scales. First the 2 larger fonts following the Fibonacci sequence (fontsize * φ; fonsize * φ^2), then the 2 smaller fonts (fontsize / φ; fontsize / φ^2), finally the original size, followed again by the 2 larger fonts. If you wait 2 seconds, the sequence clears and starts from which ever font size you are on. So if you want the 3rd larges font, then toggle twice, wait 2 sec, then toggle again. 2) In case you select a single rectangle, the script will open the "Golden Grid", "Golden Spiral" window, where you can set up the type of grid or spiral you want to insert into the document.  Gravitational point of spiral: $$\left[x,y\right]=\left[ x + \frac{{\text{width} \cdot \phi^2}}{{\phi^2 + 1}}\;, \; y + \frac{{\text{height} \cdot \phi^2}}{{\phi^2 + 1}} \right]$$ Dimensions of inner rectangles in case of Double Spiral: $$[width, height] = \left[\frac{width\cdot(\phi^2+1)}{2\phi^2}\;, \;\frac{height\cdot(\phi^2+1)}{2\phi^2}\right]$$ ```js*/ if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.4.0")) { new Notice("This script requires a newer version of Excalidraw. Please install the latest version."); return; } const phi = (1 + Math.sqrt(5)) / 2; // Golden Ratio (φ) const inversePhi = (1-1/phi); const pointsPerCurve = 20; // Number of points per curve segment const ownerWindow = ea.targetView.ownerWindow; const hostLeaf = ea.targetView.leaf; let dirty = false; const ids = []; const textEls = ea.getViewSelectedElements().filter(el=>el.type === "text"); let rect = ea.getViewSelectedElements().length === 1 ? ea.getViewSelectedElement() : null; if(!rect || rect.type !== "rectangle") { //Fontsize cycle if(textEls.length>0) { if(window.excalidrawGoldenRatio) { clearTimeout(window.excalidrawGoldenRatio?.timer); } else { window.excalidrawGoldenRatio = {timer: null, cycle:-1}; } window.excalidrawGoldenRatio.timer = setTimeout(()=>{delete window.excalidrawGoldenRatio;},2000); window.excalidrawGoldenRatio.cycle = (window.excalidrawGoldenRatio.cycle+1)%5; ea.copyViewElementsToEAforEditing(textEls); ea.getElements().forEach(el=> { el.fontSize = window.excalidrawGoldenRatio.cycle === 2 ? el.fontSize / Math.pow(phi,4) : el.fontSize * phi; ea.style.fontFamily = el.fontFamily; ea.style.fontSize = el.fontSize; const {width, height } = ea.measureText(el.originalText); el.width = width; el.height = height; }); ea.addElementsToView(); return; } new Notice("Select text elements, or a select a single rectangle"); return; } ea.copyViewElementsToEAforEditing([rect]); rect = ea.getElement(rect.id); ea.style.strokeColor = rect.strokeColor; ea.style.strokeWidth = rect.strokeWidth; ea.style.roughness = rect.roughness; ea.style.angle = rect.angle; let {x,y,width,height} = rect; // -------------------------------------------- // Load Settings // -------------------------------------------- let settings = ea.getScriptSettings(); if(!settings["Horizontal Grid"]) { settings = { "Horizontal Grid" : { value: "left-right", valueset: ["none","letf-right","right-left","center-out","center-in"] }, "Vertical Grid": { value: "none", valueset: ["none","top-down","bottom-up","center-out","center-in"] }, "Size": { value: "6", valueset: ["2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"] }, "Aspect Choice": { value: "none", valueset: ["none","adjust-width","adjust-height"] }, "Type": "grid", "Spiral Orientation": { value: "top-left", valueset: ["double","top-left","top-right","bottom-right","bottom-left"] }, "Lock Elements": false, "Send to Back": false, "Update Style": false, "Bold Spiral": false, }; await ea.setScriptSettings(settings); } let hDirection = settings["Horizontal Grid"].value; let vDirection = settings["Vertical Grid"].value; let aspectChoice = settings["Aspect Choice"].value; let type = settings["Type"]; let spiralOrientation = settings["Spiral Orientation"].value; let lockElements = settings["Lock Elements"]; let sendToBack = settings["Send to Back"]; let size = parseInt(settings["Size"].value); let updateStyle = settings["Update Style"]; let boldSpiral = settings["Bold Spiral"]; // -------------------------------------------- // Rotation // -------------------------------------------- let centerX, centerY; const rotatePointAndAddToElementList = (elementID) => { ids.push(elementID); const line = ea.getElement(elementID); // Calculate the initial position of the line's center const lineCenterX = line.x + line.width / 2; const lineCenterY = line.y + line.height / 2; // Calculate the difference between the line's center and the rectangle's center const diffX = lineCenterX - (rect.x + rect.width / 2); const diffY = lineCenterY - (rect.y + rect.height / 2); // Apply the rotation to the difference const cosTheta = Math.cos(rect.angle); const sinTheta = Math.sin(rect.angle); const rotatedX = diffX * cosTheta - diffY * sinTheta; const rotatedY = diffX * sinTheta + diffY * cosTheta; // Calculate the new position of the line's center with respect to the rectangle's center const newLineCenterX = rotatedX + (rect.x + rect.width / 2); const newLineCenterY = rotatedY + (rect.y + rect.height / 2); // Update the line's coordinates by adjusting for the change in the center line.x += newLineCenterX - lineCenterX; line.y += newLineCenterY - lineCenterY; } const rotatePointsWithinRectangle = (points) => { const centerX = rect.x + rect.width / 2; const centerY = rect.y + rect.height / 2; const cosTheta = Math.cos(rect.angle); const sinTheta = Math.sin(rect.angle); const rotatedPoints = points.map(([x, y]) => { // Translate the point relative to the rectangle's center const translatedX = x - centerX; const translatedY = y - centerY; // Apply the rotation to the translated coordinates const rotatedX = translatedX * cosTheta - translatedY * sinTheta; const rotatedY = translatedX * sinTheta + translatedY * cosTheta; // Translate back to the original coordinate system const finalX = rotatedX + centerX; const finalY = rotatedY + centerY; return [finalX, finalY]; }); return rotatedPoints; } // -------------------------------------------- // Grid // -------------------------------------------- const calculateGoldenSum = (baseOfGoldenGrid, pow) => { const ratio = 1 / phi; const geometricSum = baseOfGoldenGrid * ((1 - Math.pow(ratio, pow)) / (1 - ratio)); return geometricSum; }; const findBaseForGoldenGrid = (targetValue, n, scenario) => { const ratio = 1 / phi; if (scenario === "center-out") { return targetValue * (2-2*ratio) / (1 + ratio + 2*Math.pow(ratio,n)); } else if (scenario === "center-in") { return targetValue*2*(1-ratio)*Math.pow(phi,n-1) /(2*Math.pow(phi,n-1)*(1-Math.pow(ratio,n))-1+ratio); } else { return targetValue * (1-ratio)/(1-Math.pow(ratio,n)); } } const calculateOffsetVertical = (scenario, base) => { if (scenario === "center-out") return base / 2; if (scenario === "center-in") return base / Math.pow(phi, size + 1) / 2; return 0; }; const horizontal = (direction, scenario) => { const base = findBaseForGoldenGrid(width, size + 1, scenario); const totalGridWidth = calculateGoldenSum(base, size + 1); for (i = 1; i <= size; i++) { const offset = scenario === "center-out" ? totalGridWidth - calculateGoldenSum(base, i) : calculateGoldenSum(base, size + 1 - i); const x2 = direction === "left" ? x + offset : x + width - offset; rotatePointAndAddToElementList( ea.addLine([ [x2, y], [x2, y + height], ]) ); } }; const vertical = (direction, scenario) => { const base = findBaseForGoldenGrid(height, size + 1, scenario); const totalGridWidth = calculateGoldenSum(base, size + 1); for (i = 1; i <= size; i++) { const offset = scenario === "center-out" ? totalGridWidth - calculateGoldenSum(base, i) : calculateGoldenSum(base, size + 1 - i); const y2 = direction === "top" ? y + offset : y + height - offset; rotatePointAndAddToElementList( ea.addLine([ [x, y2], [x+width, y2], ]) ); } }; const centerHorizontal = (scenario) => { width = width / 2; horizontal("left", scenario); x += width; horizontal("right", scenario); x -= width; width = 2*width; }; const centerVertical = (scenario) => { height = height / 2; vertical("top", scenario); y += height; vertical("bottom", scenario); y -= height; height = 2*height; }; const drawGrid = () => { switch(hDirection) { case "none": break; case "left-right": horizontal("left"); break; case "right-left": horizontal("right"); break; case "center-out": centerHorizontal("center-out"); break; case "center-in": centerHorizontal("center-in"); break; } switch(vDirection) { case "none": break; case "top-down": vertical("top"); break; case "bottom-up": vertical("bottom"); break; case "center-out": centerVertical("center-out"); break; case "center-in": centerVertical("center-in"); break; } } // -------------------------------------------- // Draw Spiral // -------------------------------------------- const drawSpiral = () => { let nextX, nextY, nextW, nextH; let spiralPoints = []; let curveEndX, curveEndY, curveX, curveY; const phaseShift = { "bottom-right": 0, "bottom-left": 2, "top-left": 2, "top-right": 0, }[spiralOrientation]; let curveStartX = { "bottom-right": x, "bottom-left": x+width, "top-left": x+width, "top-right": x, }[spiralOrientation]; let curveStartY = { "bottom-right": y+height, "bottom-left": y+height, "top-left": y, "top-right": y, }[spiralOrientation]; const mirror = spiralOrientation === "bottom-left" || spiralOrientation === "top-right"; for (let i = phaseShift; i < size+phaseShift; i++) { const curvePhase = i%4; const linePhase = mirror?[0,3,2,1][curvePhase]:curvePhase; const longHorizontal = width/phi; const shortHorizontal = width*inversePhi; const longVertical = height/phi; const shortVertical = height*inversePhi; switch(linePhase) { case 0: //right nextX = x + longHorizontal; nextY = y; nextW = shortHorizontal; nextH = height; break; case 1: //down nextX = x; nextY = y + longVertical; nextW = width; nextH = shortVertical; break; case 2: //left nextX = x; nextY = y; nextW = shortHorizontal; nextH = height; break; case 3: //up nextX = x; nextY = y; nextW = width; nextH = shortVertical; break; } switch(curvePhase) { case 0: //right curveEndX = nextX; curveEndY = mirror ? nextY + nextH : nextY; break; case 1: //down curveEndX = nextX + nextW; curveEndY = mirror ? nextY + nextH : nextY; break; case 2: //left curveEndX = nextX + nextW; curveEndY = mirror ? nextY : nextY + nextH; break; case 3: //up curveEndX = nextX; curveEndY = mirror ? nextY : nextY + nextH; break; } // Add points for the curve segment for (let j = 0; j <= pointsPerCurve; j++) { const t = j / pointsPerCurve; const angle = -Math.PI / 2 * t; switch(curvePhase) { case 0: curveX = curveEndX + (curveStartX - curveEndX) * Math.cos(angle); curveY = curveStartY + (curveStartY - curveEndY) * Math.sin(angle); break; case 1: curveX = curveStartX + (curveStartX - curveEndX) * Math.sin(angle); curveY = curveEndY + (curveStartY - curveEndY) * Math.cos(angle); break; case 2: curveX = curveEndX + (curveStartX - curveEndX) * Math.cos(angle); curveY = curveStartY + (curveStartY - curveEndY) * Math.sin(angle); break; case 3: curveX = curveStartX + (curveStartX - curveEndX) * Math.sin(angle); curveY = curveEndY + (curveStartY - curveEndY) * Math.cos(angle); break; } spiralPoints.push([curveX, curveY]); } x = nextX; y = nextY; curveStartX = curveEndX; curveStartY = curveEndY; width = nextW; height = nextH; switch(linePhase) { case 0: rotatePointAndAddToElementList(ea.addLine([[x,y],[x,y+height]]));break; case 1: rotatePointAndAddToElementList(ea.addLine([[x,y],[x+width,y]]));break; case 2: rotatePointAndAddToElementList(ea.addLine([[x+width,y],[x+width,y+height]]));break; case 3: rotatePointAndAddToElementList(ea.addLine([[x,y+height],[x+width,y+height]]));break; } } const strokeWidth = ea.style.strokeWidth; ea.style.strokeWidth = strokeWidth * (boldSpiral ? 3 : 1); const angle = ea.style.angle; ea.style.angle = 0; ids.push(ea.addLine(rotatePointsWithinRectangle(spiralPoints))); ea.style.angle = angle; ea.style.strokeWidth = strokeWidth; } // -------------------------------------------- // Update Aspect Ratio // -------------------------------------------- const updateAspectRatio = () => { switch(aspectChoice) { case "none": break; case "adjust-width": rect.width = rect.height/phi; break; case "adjust-height": rect.height = rect.width/phi; break; } ({x,y,width,height} = rect); centerX = x + width/2; centerY = y + height/2; } // -------------------------------------------- // UI // -------------------------------------------- draw = async () => { if(updateStyle) { ea.style.strokeWidth = 0.5; rect.strokeWidth; ea.style.roughness = 0; rect.roughness; ea.style.roundness = null; rect.strokeWidth = 0.5; rect.roughness = 0; rect.roundness = null; } updateAspectRatio(); switch(type) { case "grid": drawGrid(); break; case "spiral": if(spiralOrientation === "double") { wInner = width * (Math.pow(phi,2)+1)/(2*Math.pow(phi,2)); hInner = height * (Math.pow(phi,2)+1)/(2*Math.pow(phi,2)); x2 = width - wInner + x; y2 = height - hInner + y; width = wInner; height = hInner; rotatePointAndAddToElementList(ea.addRect(x,y,width,height)); spiralOrientation = "bottom-right"; drawSpiral(); x = x2; y = y2; width = wInner; height = hInner; rotatePointAndAddToElementList(ea.addRect(x,y,width,height)); spiralOrientation = "top-left"; drawSpiral(); spiralOrientation = "double"; } else { drawSpiral(); } break; } ea.addToGroup(ids); ids.push(rect.id); ea.addToGroup(ids); lockElements && ea.getElements().forEach(el=>{el.locked = true;}); await ea.addElementsToView(false,false,!sendToBack); !lockElements && ea.selectElementsInView(ea.getViewElements().filter(el => ids.includes(el.id))); } const modal = new ea.obsidian.Modal(app); const fragWithHTML = (html) => createFragment((frag) => (frag.createDiv().innerHTML = html)); const keydownListener = (e) => { if(hostLeaf !== app.workspace.activeLeaf) return; if(hostLeaf.width === 0 && hostLeaf.height === 0) return; if(e.key === "Enter" && (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey)) { e.preventDefault(); modal.close(); draw() } } ownerWindow.addEventListener('keydown',keydownListener); modal.onOpen = async () => { const contentEl = modal.contentEl; contentEl.createEl("h1", {text: "Golden Ratio"}); new ea.obsidian.Setting(contentEl) .setName("Adjust Rectangle Aspect Ratio to Golden Ratio") .addDropdown(dropdown=>dropdown .addOption("none","None") .addOption("adjust-width","Adjust Width") .addOption("adjust-height","Adjust Height") .setValue(aspectChoice) .onChange(value => { aspectChoice = value; dirty = true; }) ); new ea.obsidian.Setting(contentEl) .setName("Change Line Style To: thin, architect, sharp") .addToggle(toggle=> toggle .setValue(updateStyle) .onChange(value => { dirty = true; updateStyle = value; }) ) let sizeEl; new ea.obsidian.Setting(contentEl) .setName("Number of lines") .addSlider(slider => slider .setLimits(2, 20, 1) .setValue(size) .onChange(value => { sizeEl.innerText = ` ${value.toString()}`; size = value; dirty = true; }), ) .settingEl.createDiv("", el => { sizeEl = el; el.style.minWidth = "2.3em"; el.style.textAlign = "right"; el.innerText = ` ${size.toString()}`; }); new ea.obsidian.Setting(contentEl) .setName("Lock Rectangle and Gridlines") .addToggle(toggle=> toggle .setValue(lockElements) .onChange(value => { dirty = true; lockElements = value; }) ) new ea.obsidian.Setting(contentEl) .setName("Send to Back") .addToggle(toggle=> toggle .setValue(sendToBack) .onChange(value => { dirty = true; sendToBack = value; }) ) let bGrid, bSpiral; let sHGrid, sVGrid, sSpiral, sBoldSpiral; const showGridSettings = (value) => { value ? (bGrid.setCta(), bSpiral.removeCta()) : (bGrid.removeCta(), bSpiral.setCta()); sHGrid.settingEl.style.display = value ? "" : "none"; sVGrid.settingEl.style.display = value ? "" : "none"; sSpiral.settingEl.style.display = !value ? "" : "none"; sBoldSpiral.settingEl.style.display = !value ? "" : "none"; } new ea.obsidian.Setting(contentEl) .setName(fragWithHTML("