--- name: 文本高级渲染引擎 description: 后台引擎:提供文本级的自定义渲染,支持局部加粗、局部变色、渐变色及特殊字体样式(支持混合叠加)。 author: ymjr version: 1.0.0 license: MIT usage: 后台常驻引擎。自动拦截包含 `customData.boldPartial`、`customData.colorPartial` 等字段的文本元素的渲染过程,并在 Excalidraw 原生 Canvas 层进行自定义绘制,实现富文本级别的展现效果。 features: - 拦截 `drawElementOnCanvasTextBefore` 与 `drawElementOnCanvasTextAfter` 钩子 - 支持多段局部样式 (Bold/Color) 嵌套叠加渲染 - 支持读取全局字重 (`font.weight`) 与渐变色 (`gradient`) 配置 dependencies: - 作为基础引擎供所有 `Action-Format-Text-*` 动作脚本调用 autorun: true --- /* ```javascript */ var locales = { zh: { log_fail: "EA_Core 未运行", log_gradient_error: "[Text Render Engine] 渐变解析失败:", log_unmounted: "[{id}] 🔌 已卸载", log_mounted: "[{id}] 🚀 挂载完毕" }, en: { log_fail: "EA_Core not running", log_gradient_error: "[Text Render Engine] Gradient eval failed:", log_unmounted: "[{id}] 🔌 Unmounted", log_mounted: "[{id}] 🚀 Mounted successfully" } }; const SCRIPT_ID = "ymjr.feature.text-render-engine"; const handleTextBefore = (contextPayload) => { const { ea, api } = contextPayload; if (!ea || !api) return; const { element, context, lines, horizontalOffset, verticalOffset, lineHeightPx } = contextPayload; if (!element.customData) return false; // --- 1. 全局字体与样式覆写 --- if (element.customData.bold || element.customData.title || element.customData.font || element.customData.gradient) { context.save(); if (element.customData.bold) { context.font = `bold ${context.font}`; } if (element.customData.title) { const sizeMatch = context.font.match(/\d*\.*\d+px/); if (sizeMatch) context.font = `${sizeMatch[0]} caption`; } if (element.customData.font) { if (element.customData.font.weight) context.font = `${element.customData.font.weight} ${context.font}`; if (element.customData.font.italic) context.font = `italic ${context.font}`; } // 处理渐变色 if (element.customData.gradient) { try { const runGradient = new Function('ctx', 'el', ` ${element.customData.gradient}; return gradient; `); context.fillStyle = runGradient(context, element); } catch (e) { console.error(t("log_gradient_error"), e); } } } // --- 2. 局部混合样式渲染 (加粗 + 颜色 叠加支持) --- if (element.customData.boldPartial || element.customData.colorPartial) { const bolds = element.customData.boldPartial || []; const colors = element.customData.colorPartial || []; let count = 0; // 记录不含换行符的字符绝对索引 for (let index = 0; index < lines.length; index++) { const line = lines[index]; let x = horizontalOffset; const lineStart = count; const lineEnd = count + line.length; // 提取当前行所有的“样式变化端点” let breakPoints = new Set([0, line.length]); bolds.forEach(b => { if (b.start > lineStart && b.start < lineEnd) breakPoints.add(b.start - lineStart); if (b.end > lineStart && b.end < lineEnd) breakPoints.add(b.end - lineStart); }); colors.forEach(c => { if (c.start > lineStart && c.start < lineEnd) breakPoints.add(c.start - lineStart); if (c.end > lineStart && c.end < lineEnd) breakPoints.add(c.end - lineStart); }); // 排序生成连续的切割分段 let pts = Array.from(breakPoints).sort((a, b) => a - b); for (let i = 0; i < pts.length - 1; i++) { let segStart = pts[i]; let segEnd = pts[i + 1]; let segText = line.substring(segStart, segEnd); if (!segText) continue; // 计算当前切片在整个字符串中的绝对坐标 let absoluteStart = lineStart + segStart; // 判断当前切片命中了哪些样式 let isBold = bolds.some(b => absoluteStart >= b.start && absoluteStart < b.end); // 如果有重叠颜色,优先取最后设置的那一个(反转数组查找) let colorObj = [...colors].reverse().find(c => absoluteStart >= c.start && absoluteStart < c.end); // 独立渲染当前切片 context.save(); if (isBold) { context.font = `bold ${context.font}`; } if (colorObj) { context.fillStyle = colorObj.color; context.strokeStyle = colorObj.color; } context.fillText(segText, x, index * lineHeightPx + verticalOffset); x += context.measureText(segText).width; context.restore(); } // 每行结束,增加该行的字符数 (旧脚本逻辑不计算 \n 占位) count += line.length; } return true; // 彻底接管默认文字渲染 } return false; // 如果既没有全局也没有局部,交由 Excalidraw 原生处理 }; const handleTextAfter = (contextPayload) => { const { ea, api } = contextPayload; if (!ea || !api) return; const { element, context } = contextPayload; if (element.customData && (element.customData.bold || element.customData.title || element.customData.font || element.customData.gradient)) { context.restore(); } return false; }; async function mountFeature() { if (!window.EA_Core) return console.warn(t("log_fail")); if (typeof ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`] === "function") ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`](); window.EA_Core.registerHook(SCRIPT_ID, 'drawElementOnCanvasTextBefore', handleTextBefore, 60); window.EA_Core.registerHook(SCRIPT_ID, 'drawElementOnCanvasTextAfter', handleTextAfter, 60); ExcalidrawAutomate.plugin[`disable_${SCRIPT_ID}`] = () => { if (window.EA_Core) window.EA_Core.unregisterHook(SCRIPT_ID); console.log(t("log_unmounted", { id: SCRIPT_ID })); }; console.log(t("log_mounted", { id: SCRIPT_ID })); } mountFeature();