@sliiu/present-renderer 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/COPYRIGHT +5 -0
  2. package/LICENSE +661 -0
  3. package/THIRD-PARTY-NOTICES.md +221 -0
  4. package/dist/cjs/arrow-geom.js +38 -0
  5. package/dist/cjs/canvas-backend.js +1368 -0
  6. package/dist/cjs/cell-borders.js +25 -0
  7. package/dist/cjs/chart-canvas.js +1059 -0
  8. package/dist/cjs/chart-ex-render.js +275 -0
  9. package/dist/cjs/chart-geom.js +313 -0
  10. package/dist/cjs/chart-svg.js +1110 -0
  11. package/dist/cjs/compose.js +74 -0
  12. package/dist/cjs/effect-clip.js +652 -0
  13. package/dist/cjs/entrance-hide.js +63 -0
  14. package/dist/cjs/font-stack.js +103 -0
  15. package/dist/cjs/hit-geom.js +50 -0
  16. package/dist/cjs/hybrid-backend.js +93 -0
  17. package/dist/cjs/hybrid-segments.js +54 -0
  18. package/dist/cjs/index.js +70 -0
  19. package/dist/cjs/linked-src.js +116 -0
  20. package/dist/cjs/metafile-preview.js +142 -0
  21. package/dist/cjs/model3d-frame.js +199 -0
  22. package/dist/cjs/model3d-render.js +78 -0
  23. package/dist/cjs/morph-transition.js +57 -0
  24. package/dist/cjs/node-affine.js +79 -0
  25. package/dist/cjs/ole-render.js +291 -0
  26. package/dist/cjs/package.json +1 -0
  27. package/dist/cjs/paint-canvas.js +701 -0
  28. package/dist/cjs/paint-svg.js +674 -0
  29. package/dist/cjs/scene-node.js +762 -0
  30. package/dist/cjs/selection.js +130 -0
  31. package/dist/cjs/shape-effects.js +66 -0
  32. package/dist/cjs/shape-front.js +38 -0
  33. package/dist/cjs/stage-chrome.js +15 -0
  34. package/dist/cjs/svg-backend.js +2172 -0
  35. package/dist/cjs/text-autofit.js +48 -0
  36. package/dist/cjs/text-hit.js +284 -0
  37. package/dist/cjs/text-layout.js +408 -0
  38. package/dist/cjs/text-shared.js +238 -0
  39. package/dist/cjs/text-stacked.js +82 -0
  40. package/dist/cjs/text-warp.js +377 -0
  41. package/dist/cjs/text-wordart.js +33 -0
  42. package/dist/cjs/video-frame.js +113 -0
  43. package/dist/cjs/webgl-model3d-backend.js +285 -0
  44. package/dist/cjs/webgl-shape-backend.js +360 -0
  45. package/dist/esm/arrow-geom.js +33 -0
  46. package/dist/esm/canvas-backend.js +1363 -0
  47. package/dist/esm/cell-borders.js +21 -0
  48. package/dist/esm/chart-canvas.js +1056 -0
  49. package/dist/esm/chart-ex-render.js +261 -0
  50. package/dist/esm/chart-geom.js +296 -0
  51. package/dist/esm/chart-svg.js +1107 -0
  52. package/dist/esm/compose.js +71 -0
  53. package/dist/esm/effect-clip.js +648 -0
  54. package/dist/esm/entrance-hide.js +59 -0
  55. package/dist/esm/font-stack.js +100 -0
  56. package/dist/esm/hit-geom.js +45 -0
  57. package/dist/esm/hybrid-backend.js +89 -0
  58. package/dist/esm/hybrid-segments.js +51 -0
  59. package/dist/esm/index.js +24 -0
  60. package/dist/esm/linked-src.js +109 -0
  61. package/dist/esm/metafile-preview.js +135 -0
  62. package/dist/esm/model3d-frame.js +158 -0
  63. package/dist/esm/model3d-render.js +73 -0
  64. package/dist/esm/morph-transition.js +54 -0
  65. package/dist/esm/node-affine.js +71 -0
  66. package/dist/esm/ole-render.js +286 -0
  67. package/dist/esm/paint-canvas.js +693 -0
  68. package/dist/esm/paint-svg.js +670 -0
  69. package/dist/esm/scene-node.js +750 -0
  70. package/dist/esm/selection.js +122 -0
  71. package/dist/esm/shape-effects.js +60 -0
  72. package/dist/esm/shape-front.js +35 -0
  73. package/dist/esm/stage-chrome.js +12 -0
  74. package/dist/esm/svg-backend.js +2168 -0
  75. package/dist/esm/text-autofit.js +44 -0
  76. package/dist/esm/text-hit.js +280 -0
  77. package/dist/esm/text-layout.js +400 -0
  78. package/dist/esm/text-shared.js +221 -0
  79. package/dist/esm/text-stacked.js +78 -0
  80. package/dist/esm/text-warp.js +371 -0
  81. package/dist/esm/text-wordart.js +30 -0
  82. package/dist/esm/video-frame.js +109 -0
  83. package/dist/esm/webgl-model3d-backend.js +248 -0
  84. package/dist/esm/webgl-shape-backend.js +323 -0
  85. package/dist/types/arrow-geom.d.ts +10 -0
  86. package/dist/types/canvas-backend.d.ts +55 -0
  87. package/dist/types/cell-borders.d.ts +9 -0
  88. package/dist/types/chart-canvas.d.ts +2 -0
  89. package/dist/types/chart-ex-render.d.ts +79 -0
  90. package/dist/types/chart-geom.d.ts +112 -0
  91. package/dist/types/chart-svg.d.ts +2 -0
  92. package/dist/types/compose.d.ts +2 -0
  93. package/dist/types/effect-clip.d.ts +10 -0
  94. package/dist/types/entrance-hide.d.ts +16 -0
  95. package/dist/types/font-stack.d.ts +9 -0
  96. package/dist/types/hit-geom.d.ts +15 -0
  97. package/dist/types/hybrid-backend.d.ts +21 -0
  98. package/dist/types/hybrid-segments.d.ts +11 -0
  99. package/dist/types/index.d.ts +24 -0
  100. package/dist/types/linked-src.d.ts +13 -0
  101. package/dist/types/metafile-preview.d.ts +11 -0
  102. package/dist/types/model3d-frame.d.ts +8 -0
  103. package/dist/types/model3d-render.d.ts +4 -0
  104. package/dist/types/morph-transition.d.ts +15 -0
  105. package/dist/types/node-affine.d.ts +22 -0
  106. package/dist/types/ole-render.d.ts +7 -0
  107. package/dist/types/paint-canvas.d.ts +22 -0
  108. package/dist/types/paint-svg.d.ts +19 -0
  109. package/dist/types/scene-node.d.ts +128 -0
  110. package/dist/types/selection.d.ts +22 -0
  111. package/dist/types/shape-effects.d.ts +13 -0
  112. package/dist/types/shape-front.d.ts +4 -0
  113. package/dist/types/stage-chrome.d.ts +8 -0
  114. package/dist/types/svg-backend.d.ts +88 -0
  115. package/dist/types/text-autofit.d.ts +7 -0
  116. package/dist/types/text-hit.d.ts +7 -0
  117. package/dist/types/text-layout.d.ts +35 -0
  118. package/dist/types/text-shared.d.ts +86 -0
  119. package/dist/types/text-stacked.d.ts +11 -0
  120. package/dist/types/text-warp.d.ts +37 -0
  121. package/dist/types/text-wordart.d.ts +18 -0
  122. package/dist/types/video-frame.d.ts +2 -0
  123. package/dist/types/webgl-model3d-backend.d.ts +25 -0
  124. package/dist/types/webgl-shape-backend.d.ts +33 -0
  125. package/package.json +33 -4
  126. package/README.md +0 -3
@@ -0,0 +1,670 @@
1
+ import { arrowPathD, arrowRefX, arrowSize } from "./arrow-geom.js";
2
+ import { displayImageSrc } from "./linked-src.js";
3
+ import { getImage, rasterizedAdjustHref } from "./paint-canvas.js";
4
+ import { previewImageSrc } from "./metafile-preview.js";
5
+ const SVG_NS = "http://www.w3.org/2000/svg";
6
+ let _gid = 0;
7
+ const nextGid = () => `pp-${++_gid}`;
8
+ // 平铺(tile)填充需要图片的"自然像素尺寸"作为重复单元(再乘 sx/sy)。SVG <pattern>
9
+ // 是同步生成的,拿不到异步加载的图片尺寸,所以这里维护一个尺寸缓存:首次渲染若未知,
10
+ // 后台 new Image() 加载,加载完写缓存并回调 onAsset 触发后端重绘,用真实尺寸再画一遍。
11
+ const imageSizeCache = new Map();
12
+ const imageSizeLoading = new Set();
13
+ function getImageSize(src, onAsset) {
14
+ const cached = imageSizeCache.get(src);
15
+ if (cached)
16
+ return cached;
17
+ if (typeof Image !== "undefined" && !imageSizeLoading.has(src)) {
18
+ imageSizeLoading.add(src);
19
+ const img = new Image();
20
+ if (!src.startsWith("data:") && !src.startsWith("blob:"))
21
+ img.crossOrigin = "anonymous";
22
+ img.addEventListener("load", () => {
23
+ imageSizeLoading.delete(src);
24
+ if (img.naturalWidth > 0)
25
+ imageSizeCache.set(src, { w: img.naturalWidth, h: img.naturalHeight });
26
+ onAsset?.();
27
+ }, { once: true });
28
+ img.addEventListener("error", () => imageSizeLoading.delete(src), { once: true });
29
+ img.src = src;
30
+ }
31
+ return undefined;
32
+ }
33
+ function appendStops(grad, stops) {
34
+ for (const s of stops) {
35
+ const stop = document.createElementNS(SVG_NS, "stop");
36
+ stop.setAttribute("offset", String(s.offset));
37
+ stop.setAttribute("stop-color", s.color);
38
+ if (s.alpha !== undefined)
39
+ stop.setAttribute("stop-opacity", String(s.alpha));
40
+ grad.appendChild(stop);
41
+ }
42
+ }
43
+ // OOXML flip controls how the gradient repeats outside its primary region.
44
+ // SVG spreadMethod="reflect" is the closest analogue for any flip value;
45
+ // SVG does not natively distinguish x/y/xy flip directions.
46
+ function applyGradientFlip(grad, flip) {
47
+ if (flip && flip !== "none")
48
+ grad.setAttribute("spreadMethod", "reflect");
49
+ }
50
+ /**
51
+ * shapeHint:承载该填充的形状轮廓类别,仅影响 gradFill path="shape" 的解释
52
+ * (ECMA-376 §20.1.8.30 <a:path path="shape"> = 渐变等值线沿形状轮廓)——
53
+ * 矩形轮廓走精确的"矩形框带"渲染,椭圆等其它轮廓走椭圆 radial 近似。
54
+ */
55
+ export function renderPaint(paint, defs, bbox, onAsset, shapeHint) {
56
+ if (!paint || paint.kind === "none")
57
+ return { paint: "none" };
58
+ if (paint.kind === "solid") {
59
+ return { paint: paint.color, opacity: paint.alpha };
60
+ }
61
+ if (paint.kind === "linear") {
62
+ const id = nextGid();
63
+ const grad = document.createElementNS(SVG_NS, "linearGradient");
64
+ grad.setAttribute("id", id);
65
+ grad.setAttribute("gradientUnits", "objectBoundingBox");
66
+ // OOXML angle: 0 = →, 90 = ↓. SVG x1,y1 → x2,y2 along same convention.
67
+ const a = (paint.angle * Math.PI) / 180;
68
+ const x1 = 0.5 - Math.cos(a) / 2;
69
+ const y1 = 0.5 - Math.sin(a) / 2;
70
+ const x2 = 0.5 + Math.cos(a) / 2;
71
+ const y2 = 0.5 + Math.sin(a) / 2;
72
+ grad.setAttribute("x1", String(x1));
73
+ grad.setAttribute("y1", String(y1));
74
+ grad.setAttribute("x2", String(x2));
75
+ grad.setAttribute("y2", String(y2));
76
+ applyGradientFlip(grad, paint.flip);
77
+ appendStops(grad, paint.stops);
78
+ defs.appendChild(grad);
79
+ return { paint: `url(#${id})`, defNode: grad, defId: id };
80
+ }
81
+ if (paint.kind === "radial") {
82
+ // ECMA-376 §20.1.8.30 <a:path path="…">:等值线形状。
83
+ // circle → 圆形等值线;rect → 矩形框带;shape → 沿形状轮廓。
84
+ // path="rect",以及 path="shape" 且承载形状是矩形类 → 精确的矩形框带渐变
85
+ // (4 个梯形各自铺线性渐变,等值线即嵌套矩形,接缝处参数一致无缝)。
86
+ const gpath = paint.path ?? "circle";
87
+ const rectLike = gpath === "rect" || (gpath === "shape" && shapeHint === "rect");
88
+ if (rectLike && bbox && bbox.w > 0 && bbox.h > 0) {
89
+ return renderRectFrameGradient(paint, defs, bbox);
90
+ }
91
+ const id = nextGid();
92
+ const grad = document.createElementNS(SVG_NS, "radialGradient");
93
+ grad.setAttribute("id", id);
94
+ // fillToRect = focus rect for path gradient (ECMA-376 §20.1.8.30).
95
+ // OOXML semantics: stop pos=0 is at fillToRect center; pos=100% is at path edge.
96
+ // SVG radialGradient: fx/fy = offset 0 focus; cx/cy/r = offset 100% circle.
97
+ // Centering cx/cy on fx/fy and choosing r as max distance to bbox corners
98
+ // ensures the last stop reaches the shape boundary.
99
+ const { l, t, r, b } = paint.fillToRect;
100
+ const fx = (l + (1 - r)) / 2;
101
+ const fy = (t + (1 - b)) / 2;
102
+ // path="circle" 的等值线是真圆(PowerPoint 在绝对坐标里画圆)。有 bbox 时用
103
+ // userSpaceOnUse 建真圆;objectBoundingBox 会把圆各向异性拉成椭圆(非正方形
104
+ // bbox 下与 PowerPoint 不符)。path="shape" 落到这里的(椭圆/自由形状)则保留
105
+ // objectBoundingBox 椭圆——对椭圆轮廓恰好是精确解。无 bbox 时也退回旧行为。
106
+ const useUser = gpath === "circle" && !!bbox && bbox.w > 0 && bbox.h > 0;
107
+ if (useUser) {
108
+ const ufx = bbox.x + fx * bbox.w;
109
+ const ufy = bbox.y + fy * bbox.h;
110
+ const rad = Math.max(Math.hypot(ufx - bbox.x, ufy - bbox.y), Math.hypot(bbox.x + bbox.w - ufx, ufy - bbox.y), Math.hypot(ufx - bbox.x, bbox.y + bbox.h - ufy), Math.hypot(bbox.x + bbox.w - ufx, bbox.y + bbox.h - ufy));
111
+ grad.setAttribute("gradientUnits", "userSpaceOnUse");
112
+ grad.setAttribute("cx", String(ufx));
113
+ grad.setAttribute("cy", String(ufy));
114
+ grad.setAttribute("fx", String(ufx));
115
+ grad.setAttribute("fy", String(ufy));
116
+ grad.setAttribute("r", String(rad));
117
+ }
118
+ else {
119
+ const rad = Math.max(Math.hypot(fx, fy), Math.hypot(1 - fx, fy), Math.hypot(fx, 1 - fy), Math.hypot(1 - fx, 1 - fy));
120
+ grad.setAttribute("gradientUnits", "objectBoundingBox");
121
+ grad.setAttribute("cx", String(fx));
122
+ grad.setAttribute("cy", String(fy));
123
+ grad.setAttribute("fx", String(fx));
124
+ grad.setAttribute("fy", String(fy));
125
+ grad.setAttribute("r", String(rad));
126
+ }
127
+ applyGradientFlip(grad, paint.flip);
128
+ appendStops(grad, paint.stops);
129
+ defs.appendChild(grad);
130
+ return { paint: `url(#${id})`, defNode: grad, defId: id };
131
+ }
132
+ if (paint.kind === "image") {
133
+ const id = nextGid();
134
+ const pat = document.createElementNS(SVG_NS, "pattern");
135
+ pat.setAttribute("id", id);
136
+ const fit = paint.fit;
137
+ const tileSx = paint.tileSx ?? 1;
138
+ const tileSy = paint.tileSy ?? 1;
139
+ // 有 bbox → 用 userSpaceOnUse,让 image 视口按真实像素宽高比建立,
140
+ // preserveAspectRatio 的 cover(slice)/contain(meet) 才能保持图片比例。
141
+ // 旧实现用 objectBoundingBox(1×1 正方坐标系再各向异性拉到 bbox),
142
+ // 非正方形(16:9 / 4:3)背景下 cover/contain 会把图压变形。
143
+ // 无 bbox 时退回 objectBoundingBox 行为(bx=0,bw=1 → 公式与旧实现等价)。
144
+ const useUser = !!bbox;
145
+ pat.setAttribute("patternUnits", useUser ? "userSpaceOnUse" : "objectBoundingBox");
146
+ pat.setAttribute("patternContentUnits", useUser ? "userSpaceOnUse" : "objectBoundingBox");
147
+ const bx = bbox?.x ?? 0;
148
+ const by = bbox?.y ?? 0;
149
+ const bw = bbox?.w ?? 1;
150
+ const bh = bbox?.h ?? 1;
151
+ const im = document.createElementNS(SVG_NS, "image");
152
+ const probe = getImage(paint.src, onAsset);
153
+ const shown = previewImageSrc(paint.src, probe, onAsset);
154
+ let href = shown ? displayImageSrc(shown, bbox?.w ?? 320, bbox?.h ?? 180) : "";
155
+ let usedRaster = false;
156
+ if (paint.adjust && shown) {
157
+ const raster = rasterizedAdjustHref(shown, paint.adjust, onAsset);
158
+ if (raster) {
159
+ href = raster;
160
+ usedRaster = true;
161
+ }
162
+ }
163
+ im.setAttribute("href", href);
164
+ im.setAttributeNS("http://www.w3.org/1999/xlink", "href", href);
165
+ if (fit === "tile") {
166
+ // 平铺单元 = 图片自然像素 × sx/sy(slide 坐标 1px=1unit),并按 tx/ty 偏移。
167
+ // 用 userSpaceOnUse 才能用绝对像素尺寸;拿不到自然尺寸时先回退到 bbox(≈拉伸一张),
168
+ // 加载完触发重绘再用真实尺寸正确平铺。
169
+ const size = useUser && shown ? getImageSize(shown, onAsset) : undefined;
170
+ const cellW = (size ? size.w : bw) * tileSx;
171
+ const cellH = (size ? size.h : bh) * tileSy;
172
+ const ox = bx + (paint.tileTx ?? 0);
173
+ const oy = by + (paint.tileTy ?? 0);
174
+ pat.setAttribute("x", String(ox));
175
+ pat.setAttribute("y", String(oy));
176
+ pat.setAttribute("width", String(cellW));
177
+ pat.setAttribute("height", String(cellH));
178
+ if (paint.tileFlip)
179
+ pat.setAttribute("data-flip", paint.tileFlip);
180
+ im.setAttribute("preserveAspectRatio", "none");
181
+ im.setAttribute("x", String(ox));
182
+ im.setAttribute("y", String(oy));
183
+ im.setAttribute("width", String(cellW));
184
+ im.setAttribute("height", String(cellH));
185
+ }
186
+ else {
187
+ pat.setAttribute("x", String(bx));
188
+ pat.setAttribute("y", String(by));
189
+ pat.setAttribute("width", String(bw));
190
+ pat.setAttribute("height", String(bh));
191
+ const par = fit === "fill" ? "none" : fit === "cover" ? "xMidYMid slice" : "xMidYMid meet";
192
+ im.setAttribute("preserveAspectRatio", paint.srcRect ? "none" : par);
193
+ if (paint.srcRect) {
194
+ const sr = paint.srcRect;
195
+ const cw = Math.max(1e-6, 1 - sr.l - sr.r);
196
+ const ch = Math.max(1e-6, 1 - sr.t - sr.b);
197
+ const iw = bw / cw;
198
+ const ih = bh / ch;
199
+ im.setAttribute("x", String(bx - sr.l * iw));
200
+ im.setAttribute("y", String(by - sr.t * ih));
201
+ im.setAttribute("width", String(iw));
202
+ im.setAttribute("height", String(ih));
203
+ }
204
+ else {
205
+ im.setAttribute("x", String(bx));
206
+ im.setAttribute("y", String(by));
207
+ im.setAttribute("width", String(bw));
208
+ im.setAttribute("height", String(bh));
209
+ }
210
+ }
211
+ if (paint.adjust && !usedRaster) {
212
+ const filterId = ensureAdjustFilter(defs, paint.adjust);
213
+ if (filterId)
214
+ im.setAttribute("filter", `url(#${filterId})`);
215
+ }
216
+ if (paint.alpha !== undefined)
217
+ im.setAttribute("opacity", String(paint.alpha));
218
+ pat.appendChild(im);
219
+ defs.appendChild(pat);
220
+ return { paint: `url(#${id})`, defNode: pat, defId: id };
221
+ }
222
+ if (paint.kind === "pattern") {
223
+ const id = nextGid();
224
+ const pat = renderPatternFill(paint, id);
225
+ defs.appendChild(pat);
226
+ return { paint: `url(#${id})`, defNode: pat, defId: id };
227
+ }
228
+ if (paint.kind === "groupFill") {
229
+ return { paint: "none" };
230
+ }
231
+ return { paint: "none" };
232
+ }
233
+ // ECMA-376 §20.1.8.30 gradFill path="rect"(及 path="shape" + 矩形轮廓)——
234
+ // 等值线是从 fillToRect(pos=0)到形状外缘(pos=1)的嵌套矩形框。
235
+ // SVG 没有"矩形径向渐变"原语,这里用 <pattern>(userSpaceOnUse) 精确合成:
236
+ // 中心 = fillToRect 矩形铺 stop0 纯色;四周 = 4 个梯形各铺一条线性渐变
237
+ // (上下两块沿 y、左右两块沿 x)。任取一点,其"框带参数"沿 x 量和沿 y 量
238
+ // 在梯形对角接缝上相等 → 四块颜色在接缝处严格一致,视觉无缝。
239
+ // 形状自身的几何(圆角、椭圆裁剪)由 fill 所在元素天然裁剪 pattern,无需处理。
240
+ function renderRectFrameGradient(paint, defs, bbox) {
241
+ const id = nextGid();
242
+ const { x, y, w, h } = bbox;
243
+ const { l, t, r, b } = paint.fillToRect;
244
+ // focus 矩形(用户空间)。clamp 防御非法 fillToRect(l+r>1 等)翻转。
245
+ const fx1 = x + Math.min(Math.max(l, 0), 1) * w;
246
+ const fy1 = y + Math.min(Math.max(t, 0), 1) * h;
247
+ const fx2 = x + (1 - Math.min(Math.max(r, 0), 1)) * w;
248
+ const fy2 = y + (1 - Math.min(Math.max(b, 0), 1)) * h;
249
+ let cx1 = Math.min(fx1, fx2);
250
+ let cx2 = Math.max(fx1, fx2);
251
+ let cy1 = Math.min(fy1, fy2);
252
+ let cy2 = Math.max(fy1, fy2);
253
+ // fillToRect 缺省解析为 {0,0,0,0}(focus 覆盖整个形状)——按字面渲染会得到
254
+ // stop0 纯色。与旧 radial 分支保持一致:视为中心点 focus。
255
+ if (cx1 <= x && cx2 >= x + w && cy1 <= y && cy2 >= y + h) {
256
+ cx1 = cx2 = x + w / 2;
257
+ cy1 = cy2 = y + h / 2;
258
+ }
259
+ const pat = document.createElementNS(SVG_NS, "pattern");
260
+ pat.setAttribute("id", id);
261
+ pat.setAttribute("patternUnits", "userSpaceOnUse");
262
+ pat.setAttribute("patternContentUnits", "userSpaceOnUse");
263
+ pat.setAttribute("x", String(x));
264
+ pat.setAttribute("y", String(y));
265
+ pat.setAttribute("width", String(w));
266
+ pat.setAttribute("height", String(h));
267
+ const stops = paint.stops;
268
+ const s0 = stops[0];
269
+ // focus 矩形内部整体是 pos=0 的颜色。
270
+ if (s0 && cx2 > cx1 && cy2 > cy1) {
271
+ const core = document.createElementNS(SVG_NS, "rect");
272
+ core.setAttribute("x", String(cx1));
273
+ core.setAttribute("y", String(cy1));
274
+ core.setAttribute("width", String(cx2 - cx1));
275
+ core.setAttribute("height", String(cy2 - cy1));
276
+ core.setAttribute("fill", s0.color);
277
+ if (s0.alpha !== undefined)
278
+ core.setAttribute("fill-opacity", String(s0.alpha));
279
+ pat.appendChild(core);
280
+ }
281
+ // 四个梯形:points + 各自线性渐变轴(从 focus 边 pos=0 到外缘 pos=1)。
282
+ const quads = [
283
+ // top
284
+ { pts: `${x},${y} ${x + w},${y} ${cx2},${cy1} ${cx1},${cy1}`, x1: 0, y1: cy1, x2: 0, y2: y },
285
+ // bottom
286
+ { pts: `${x},${y + h} ${x + w},${y + h} ${cx2},${cy2} ${cx1},${cy2}`, x1: 0, y1: cy2, x2: 0, y2: y + h },
287
+ // left
288
+ { pts: `${x},${y} ${cx1},${cy1} ${cx1},${cy2} ${x},${y + h}`, x1: cx1, y1: 0, x2: x, y2: 0 },
289
+ // right
290
+ { pts: `${x + w},${y} ${cx2},${cy1} ${cx2},${cy2} ${x + w},${y + h}`, x1: cx2, y1: 0, x2: x + w, y2: 0 },
291
+ ];
292
+ quads.forEach((q, i) => {
293
+ // 渐变轴长度为 0(focus 边贴着外缘)→ 该侧没有框带,跳过。
294
+ if (q.x1 === q.x2 && q.y1 === q.y2)
295
+ return;
296
+ const gid = `${id}-q${i}`;
297
+ const grad = document.createElementNS(SVG_NS, "linearGradient");
298
+ grad.setAttribute("id", gid);
299
+ grad.setAttribute("gradientUnits", "userSpaceOnUse");
300
+ grad.setAttribute("x1", String(q.x1));
301
+ grad.setAttribute("y1", String(q.y1));
302
+ grad.setAttribute("x2", String(q.x2));
303
+ grad.setAttribute("y2", String(q.y2));
304
+ applyGradientFlip(grad, paint.flip);
305
+ appendStops(grad, stops);
306
+ pat.appendChild(grad);
307
+ const poly = document.createElementNS(SVG_NS, "polygon");
308
+ poly.setAttribute("points", q.pts);
309
+ poly.setAttribute("fill", `url(#${gid})`);
310
+ pat.appendChild(poly);
311
+ });
312
+ defs.appendChild(pat);
313
+ return { paint: `url(#${id})`, defNode: pat, defId: id };
314
+ }
315
+ // ECMA-376 §20.1.8.47 + ST_PresetPatternVal — 用 8x8 像素位图把 PowerPoint
316
+ // 内置图案翻译为 SVG <pattern>。所有图案都以 fg/bg 两色渲染。
317
+ function renderPatternFill(paint, id) {
318
+ const pat = document.createElementNS(SVG_NS, "pattern");
319
+ pat.setAttribute("id", id);
320
+ pat.setAttribute("patternUnits", "userSpaceOnUse");
321
+ pat.setAttribute("width", "8");
322
+ pat.setAttribute("height", "8");
323
+ const bg = document.createElementNS(SVG_NS, "rect");
324
+ bg.setAttribute("width", "8");
325
+ bg.setAttribute("height", "8");
326
+ bg.setAttribute("fill", paint.bg);
327
+ if (paint.bgAlpha !== undefined)
328
+ bg.setAttribute("fill-opacity", String(paint.bgAlpha));
329
+ pat.appendChild(bg);
330
+ const fg = paint.fg;
331
+ const fgOp = paint.fgAlpha !== undefined ? `fill-opacity="${paint.fgAlpha}"` : "";
332
+ pat.insertAdjacentHTML("beforeend", presetPatternSvg(paint.prst, fg, fgOp));
333
+ return pat;
334
+ }
335
+ function presetPatternSvg(prst, fg, fgOp) {
336
+ const dot = (x, y, s = 1) => `<rect x="${x}" y="${y}" width="${s}" height="${s}" fill="${fg}" ${fgOp}/>`;
337
+ const line = (d, w = 1) => `<path d="${d}" stroke="${fg}" stroke-width="${w}" ${fgOp.replace("fill-opacity", "stroke-opacity")} fill="none"/>`;
338
+ switch (prst) {
339
+ case "pct5": return dot(0, 0);
340
+ case "pct10": return dot(0, 0) + dot(4, 4);
341
+ case "pct20": return dot(0, 0) + dot(2, 2) + dot(4, 4) + dot(6, 6);
342
+ case "pct25": return dot(0, 0) + dot(2, 2) + dot(4, 4) + dot(6, 6) + dot(0, 4) + dot(4, 0);
343
+ case "pct30":
344
+ case "pct40":
345
+ case "pct50":
346
+ return dot(0, 0, 1) + dot(2, 2, 1) + dot(4, 4, 1) + dot(6, 6, 1) +
347
+ dot(0, 4, 1) + dot(2, 6, 1) + dot(4, 0, 1) + dot(6, 2, 1);
348
+ case "pct60":
349
+ case "pct70":
350
+ case "pct75":
351
+ case "pct80":
352
+ case "pct90":
353
+ return `<rect width="8" height="8" fill="${fg}" ${fgOp}/>`;
354
+ case "horz":
355
+ return line("M 0 4 L 8 4");
356
+ case "vert":
357
+ return line("M 4 0 L 4 8");
358
+ case "ltHorz":
359
+ return line("M 0 2 L 8 2") + line("M 0 6 L 8 6");
360
+ case "ltVert":
361
+ return line("M 2 0 L 2 8") + line("M 6 0 L 6 8");
362
+ case "dkHorz":
363
+ return line("M 0 4 L 8 4", 2);
364
+ case "dkVert":
365
+ return line("M 4 0 L 4 8", 2);
366
+ case "narHorz":
367
+ return line("M 0 1 L 8 1") + line("M 0 3 L 8 3") + line("M 0 5 L 8 5") + line("M 0 7 L 8 7");
368
+ case "narVert":
369
+ return line("M 1 0 L 1 8") + line("M 3 0 L 3 8") + line("M 5 0 L 5 8") + line("M 7 0 L 7 8");
370
+ case "dashHorz":
371
+ return line("M 0 4 L 4 4") + line("M 5 4 L 8 4");
372
+ case "dashVert":
373
+ return line("M 4 0 L 4 4") + line("M 4 5 L 4 8");
374
+ case "cross":
375
+ return line("M 0 4 L 8 4") + line("M 4 0 L 4 8");
376
+ case "dnDiag":
377
+ case "ltDnDiag":
378
+ return line("M 0 0 L 8 8") + line("M -2 6 L 2 10") + line("M 6 -2 L 10 2");
379
+ case "upDiag":
380
+ case "ltUpDiag":
381
+ return line("M 0 8 L 8 0") + line("M -2 2 L 2 -2") + line("M 6 10 L 10 6");
382
+ case "dkDnDiag":
383
+ return line("M 0 0 L 8 8", 2);
384
+ case "dkUpDiag":
385
+ return line("M 0 8 L 8 0", 2);
386
+ case "wdDnDiag":
387
+ return line("M -2 4 L 4 10") + line("M 4 -2 L 10 4");
388
+ case "wdUpDiag":
389
+ return line("M -2 4 L 4 -2") + line("M 4 10 L 10 4");
390
+ case "diagCross":
391
+ return line("M 0 0 L 8 8") + line("M 0 8 L 8 0");
392
+ case "weave":
393
+ case "trellis":
394
+ case "smGrid":
395
+ case "lgGrid":
396
+ return line("M 0 4 L 8 4") + line("M 4 0 L 4 8");
397
+ case "smCheck":
398
+ case "lgCheck":
399
+ return `<rect width="4" height="4" fill="${fg}" ${fgOp}/><rect x="4" y="4" width="4" height="4" fill="${fg}" ${fgOp}/>`;
400
+ default:
401
+ return dot(0, 0) + dot(2, 2) + dot(4, 4) + dot(6, 6);
402
+ }
403
+ }
404
+ function ensureAdjustFilter(defs, adj) {
405
+ const key = adjustCacheKey(adj);
406
+ if (!key)
407
+ return null;
408
+ const id = `imgadj-${key}`;
409
+ if (defs.querySelector(`#${CSS.escape(id)}`))
410
+ return id;
411
+ const filter = document.createElementNS(SVG_NS, "filter");
412
+ filter.setAttribute("id", id);
413
+ filter.setAttribute("x", "0");
414
+ filter.setAttribute("y", "0");
415
+ filter.setAttribute("width", "1");
416
+ filter.setAttribute("height", "1");
417
+ let lastIn = "SourceGraphic";
418
+ if (adj.grayscale) {
419
+ const fe = document.createElementNS(SVG_NS, "feColorMatrix");
420
+ fe.setAttribute("type", "matrix");
421
+ fe.setAttribute("in", lastIn);
422
+ fe.setAttribute("values", "0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0");
423
+ fe.setAttribute("result", "fxGray");
424
+ filter.appendChild(fe);
425
+ lastIn = "fxGray";
426
+ }
427
+ if (adj.lum) {
428
+ const b = adj.lum.bright ?? 0;
429
+ const c = adj.lum.contrast ?? 0;
430
+ const slope = 1 + c;
431
+ const intercept = b - c / 2;
432
+ const fe = document.createElementNS(SVG_NS, "feComponentTransfer");
433
+ fe.setAttribute("in", lastIn);
434
+ fe.setAttribute("result", "fxLum");
435
+ for (const ch of ["feFuncR", "feFuncG", "feFuncB"]) {
436
+ const f = document.createElementNS(SVG_NS, ch);
437
+ f.setAttribute("type", "linear");
438
+ f.setAttribute("slope", String(slope));
439
+ f.setAttribute("intercept", String(intercept));
440
+ fe.appendChild(f);
441
+ }
442
+ filter.appendChild(fe);
443
+ lastIn = "fxLum";
444
+ }
445
+ if (adj.hsl) {
446
+ const hue = adj.hsl.hue ?? 0;
447
+ if (hue) {
448
+ const rot = document.createElementNS(SVG_NS, "feColorMatrix");
449
+ rot.setAttribute("type", "hueRotate");
450
+ rot.setAttribute("in", lastIn);
451
+ rot.setAttribute("values", String(hue));
452
+ rot.setAttribute("result", "fxHue");
453
+ filter.appendChild(rot);
454
+ lastIn = "fxHue";
455
+ }
456
+ const sat = 1 + (adj.hsl.sat ?? 0);
457
+ if (sat !== 1) {
458
+ const s = document.createElementNS(SVG_NS, "feColorMatrix");
459
+ s.setAttribute("type", "saturate");
460
+ s.setAttribute("in", lastIn);
461
+ s.setAttribute("values", String(sat));
462
+ s.setAttribute("result", "fxSat");
463
+ filter.appendChild(s);
464
+ lastIn = "fxSat";
465
+ }
466
+ }
467
+ if (adj.duotone) {
468
+ const [c1, c2] = adj.duotone;
469
+ const gray = document.createElementNS(SVG_NS, "feColorMatrix");
470
+ gray.setAttribute("type", "matrix");
471
+ gray.setAttribute("in", lastIn);
472
+ gray.setAttribute("values", "0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0");
473
+ gray.setAttribute("result", "fxDuoG");
474
+ filter.appendChild(gray);
475
+ const rgb1 = parseHex(c1);
476
+ const rgb2 = parseHex(c2);
477
+ const mat = document.createElementNS(SVG_NS, "feColorMatrix");
478
+ mat.setAttribute("type", "matrix");
479
+ mat.setAttribute("in", "fxDuoG");
480
+ const dr = rgb2[0] - rgb1[0];
481
+ const dg = rgb2[1] - rgb1[1];
482
+ const db = rgb2[2] - rgb1[2];
483
+ mat.setAttribute("values", `${dr} 0 0 0 ${rgb1[0]} ${dg} 0 0 0 ${rgb1[1]} ${db} 0 0 0 ${rgb1[2]} 0 0 0 1 0`);
484
+ mat.setAttribute("result", "fxDuo");
485
+ filter.appendChild(mat);
486
+ lastIn = "fxDuo";
487
+ }
488
+ if (adj.biLevel !== undefined) {
489
+ const t = adj.biLevel;
490
+ const gray = document.createElementNS(SVG_NS, "feColorMatrix");
491
+ gray.setAttribute("type", "matrix");
492
+ gray.setAttribute("in", lastIn);
493
+ gray.setAttribute("values", "0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0");
494
+ gray.setAttribute("result", "fxBiG");
495
+ filter.appendChild(gray);
496
+ const fe = document.createElementNS(SVG_NS, "feComponentTransfer");
497
+ fe.setAttribute("in", "fxBiG");
498
+ fe.setAttribute("result", "fxBi");
499
+ for (const ch of ["feFuncR", "feFuncG", "feFuncB"]) {
500
+ const f = document.createElementNS(SVG_NS, ch);
501
+ f.setAttribute("type", "linear");
502
+ f.setAttribute("slope", "1000");
503
+ f.setAttribute("intercept", String(-t * 1000));
504
+ fe.appendChild(f);
505
+ }
506
+ filter.appendChild(fe);
507
+ lastIn = "fxBi";
508
+ }
509
+ if (adj.fillOverlay) {
510
+ const rgb = parseHex(adj.fillOverlay.color);
511
+ const flood = document.createElementNS(SVG_NS, "feFlood");
512
+ flood.setAttribute("flood-color", `rgb(${Math.round(rgb[0] * 255)},${Math.round(rgb[1] * 255)},${Math.round(rgb[2] * 255)})`);
513
+ flood.setAttribute("flood-opacity", adj.fillOverlay.blend === "over" ? "0.45" : "1");
514
+ flood.setAttribute("result", "fxOvFlood");
515
+ filter.appendChild(flood);
516
+ const blend = document.createElementNS(SVG_NS, "feBlend");
517
+ const mode = adj.fillOverlay.blend === "mult"
518
+ ? "multiply"
519
+ : adj.fillOverlay.blend === "screen"
520
+ ? "screen"
521
+ : adj.fillOverlay.blend === "darken"
522
+ ? "darken"
523
+ : adj.fillOverlay.blend === "lighten"
524
+ ? "lighten"
525
+ : "normal";
526
+ blend.setAttribute("in", "fxOvFlood");
527
+ blend.setAttribute("in2", lastIn);
528
+ blend.setAttribute("mode", mode);
529
+ blend.setAttribute("result", "fxOv");
530
+ filter.appendChild(blend);
531
+ lastIn = "fxOv";
532
+ }
533
+ if (adj.tint) {
534
+ const hue = adj.tint.hue ?? 0;
535
+ const amt = Math.max(0, Math.min(1, adj.tint.amount ?? 0));
536
+ if (hue) {
537
+ const rot = document.createElementNS(SVG_NS, "feColorMatrix");
538
+ rot.setAttribute("type", "hueRotate");
539
+ rot.setAttribute("in", lastIn);
540
+ rot.setAttribute("values", String(hue * amt));
541
+ rot.setAttribute("result", "fxTintH");
542
+ filter.appendChild(rot);
543
+ lastIn = "fxTintH";
544
+ }
545
+ if (amt) {
546
+ const sat = document.createElementNS(SVG_NS, "feColorMatrix");
547
+ sat.setAttribute("type", "saturate");
548
+ sat.setAttribute("in", lastIn);
549
+ sat.setAttribute("values", String(1 + amt));
550
+ sat.setAttribute("result", "fxTintS");
551
+ filter.appendChild(sat);
552
+ lastIn = "fxTintS";
553
+ }
554
+ }
555
+ if (adj.alphaMod !== undefined && adj.alphaMod < 1) {
556
+ const fe = document.createElementNS(SVG_NS, "feColorMatrix");
557
+ fe.setAttribute("type", "matrix");
558
+ fe.setAttribute("in", lastIn);
559
+ fe.setAttribute("values", `1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 ${adj.alphaMod} 0`);
560
+ filter.appendChild(fe);
561
+ }
562
+ defs.appendChild(filter);
563
+ return id;
564
+ }
565
+ function adjustCacheKey(adj) {
566
+ const parts = [];
567
+ if (adj.grayscale)
568
+ parts.push("g");
569
+ if (adj.lum)
570
+ parts.push(`l${adj.lum.bright ?? 0}_${adj.lum.contrast ?? 0}`);
571
+ if (adj.hsl)
572
+ parts.push(`h${adj.hsl.hue ?? 0}_${adj.hsl.sat ?? 0}_${adj.hsl.lum ?? 0}`);
573
+ if (adj.duotone)
574
+ parts.push(`d${adj.duotone[0]}_${adj.duotone[1]}`);
575
+ if (adj.biLevel !== undefined)
576
+ parts.push(`bi${adj.biLevel}`);
577
+ if (adj.clrChange)
578
+ parts.push(`cc${adj.clrChange.from}_${adj.clrChange.to}`);
579
+ if (adj.fillOverlay)
580
+ parts.push(`fo${adj.fillOverlay.color}_${adj.fillOverlay.blend ?? "over"}`);
581
+ if (adj.tint)
582
+ parts.push(`t${adj.tint.hue ?? 0}_${adj.tint.amount ?? 0}`);
583
+ if (adj.alphaMod !== undefined)
584
+ parts.push(`a${adj.alphaMod}`);
585
+ if (parts.length === 0)
586
+ return null;
587
+ return parts.join("-").replace(/[^a-zA-Z0-9_-]/g, "_");
588
+ }
589
+ function parseHex(c) {
590
+ const m = /^#?([0-9a-f]{6})$/i.exec(c);
591
+ if (!m)
592
+ return [0, 0, 0];
593
+ const v = m[1];
594
+ return [
595
+ parseInt(v.slice(0, 2), 16) / 255,
596
+ parseInt(v.slice(2, 4), 16) / 255,
597
+ parseInt(v.slice(4, 6), 16) / 255,
598
+ ];
599
+ }
600
+ // idPrefix 唯一标识"调用方 backend 实例",用于给 <marker> 这类 *内容派生 id*
601
+ // (arr-end-triangle-…,多 backend 间会重名)加前缀。否则 url(#arr-…) 会按文档
602
+ // 顺序解析到第一个同名 marker——放映时该 marker 往往落在 display:none 的缩略图
603
+ // SVG 里(其 render tree 不构建 → marker 不解析),导致"舞台/缩略图箭头都在,
604
+ // 一放映就没了"。gradient 等用全局自增 id(pp-N)天然唯一,不受影响。
605
+ export function applyStroke(el, stroke, defs, idPrefix = "") {
606
+ if (!stroke) {
607
+ el.setAttribute("stroke", "none");
608
+ el.removeAttribute("marker-start");
609
+ el.removeAttribute("marker-end");
610
+ return;
611
+ }
612
+ const p = renderPaint(stroke.paint, defs);
613
+ el.setAttribute("stroke", p.paint);
614
+ if (p.opacity !== undefined)
615
+ el.setAttribute("stroke-opacity", String(p.opacity));
616
+ el.setAttribute("stroke-width", String(stroke.width));
617
+ if (stroke.dash)
618
+ el.setAttribute("stroke-dasharray", stroke.dash.join(","));
619
+ if (stroke.cap)
620
+ el.setAttribute("stroke-linecap", stroke.cap);
621
+ if (stroke.join)
622
+ el.setAttribute("stroke-linejoin", stroke.join);
623
+ const tag = el.tagName.toLowerCase();
624
+ const supportsMarker = tag === "line" || tag === "path" || tag === "polyline";
625
+ if (supportsMarker && stroke.headEnd) {
626
+ const id = ensureArrowMarker(defs, markerFill(stroke), stroke.headEnd, "start", idPrefix);
627
+ el.setAttribute("marker-start", `url(#${id})`);
628
+ }
629
+ else {
630
+ el.removeAttribute("marker-start");
631
+ }
632
+ if (supportsMarker && stroke.tailEnd) {
633
+ const id = ensureArrowMarker(defs, markerFill(stroke), stroke.tailEnd, "end", idPrefix);
634
+ el.setAttribute("marker-end", `url(#${id})`);
635
+ }
636
+ else {
637
+ el.removeAttribute("marker-end");
638
+ }
639
+ }
640
+ function markerFill(stroke) {
641
+ if (stroke.paint.kind === "solid")
642
+ return stroke.paint.color;
643
+ return "context-stroke";
644
+ }
645
+ function ensureArrowMarker(defs, color, end, side, idPrefix = "") {
646
+ const { w, l } = arrowSize(end);
647
+ const id = `${idPrefix}arr-${side}-${end.kind}-${end.w ?? "med"}-${end.len ?? "med"}-${color.replace(/[^\w]/g, "_")}`;
648
+ if (defs.querySelector(`#${CSS.escape(id)}`))
649
+ return id;
650
+ const marker = document.createElementNS(SVG_NS, "marker");
651
+ marker.setAttribute("id", id);
652
+ marker.setAttribute("orient", "auto");
653
+ marker.setAttribute("markerUnits", "strokeWidth");
654
+ marker.setAttribute("markerWidth", String(l));
655
+ marker.setAttribute("markerHeight", String(w));
656
+ // oval 的 path 用相对 arc,X 范围实际为 [-l/2, l/2],需要把 viewBox 向左拓展。
657
+ const vbX = end.kind === "oval" ? -l / 2 : 0;
658
+ marker.setAttribute("viewBox", `${vbX} ${-w / 2} ${l} ${w}`);
659
+ marker.setAttribute("refX", String(arrowRefX(end.kind, side, l)));
660
+ marker.setAttribute("refY", "0");
661
+ const path = document.createElementNS(SVG_NS, "path");
662
+ path.setAttribute("d", arrowPathD(end.kind, w, l, side));
663
+ path.setAttribute("fill", color);
664
+ // marker 内的 fill 不应再继承被指 line 的 stroke;显式置为 none 避免某些浏览器
665
+ // 对 <marker> 透传父级 stroke 把箭头边描成线宽。
666
+ path.setAttribute("stroke", "none");
667
+ marker.appendChild(path);
668
+ defs.appendChild(marker);
669
+ return id;
670
+ }