@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,701 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getImage = getImage;
4
+ exports.waitForImagesLoaded = waitForImagesLoaded;
5
+ exports.paintToCanvasStyle = paintToCanvasStyle;
6
+ exports.adjustImage = adjustImage;
7
+ exports.rasterizedAdjustHref = rasterizedAdjustHref;
8
+ exports.applyStroke = applyStroke;
9
+ const linked_src_1 = require("./linked-src");
10
+ const metafile_preview_1 = require("./metafile-preview");
11
+ const video_frame_1 = require("./video-frame");
12
+ const model3d_frame_1 = require("./model3d-frame");
13
+ const imageCache = new Map();
14
+ function safeStops(stops) {
15
+ const out = [];
16
+ for (const s of stops) {
17
+ const o = s.offset;
18
+ if (!Number.isFinite(o))
19
+ continue;
20
+ out.push({ ...s, offset: o < 0 ? 0 : o > 1 ? 1 : o });
21
+ }
22
+ return out;
23
+ }
24
+ function getImage(src, onLoad) {
25
+ src = (0, linked_src_1.displayImageSrc)(src);
26
+ let img = imageCache.get(src);
27
+ if (img) {
28
+ if (onLoad && !(img.complete && img.naturalWidth > 0)) {
29
+ img.addEventListener("load", onLoad, { once: true });
30
+ }
31
+ return img;
32
+ }
33
+ img = new Image();
34
+ if (!src.startsWith("data:") && !src.startsWith("blob:"))
35
+ img.crossOrigin = "anonymous";
36
+ if (onLoad)
37
+ img.addEventListener("load", onLoad, { once: true });
38
+ img.src = src;
39
+ imageCache.set(src, img);
40
+ return img;
41
+ }
42
+ /**
43
+ * 等待 imageCache 里所有尚未落定的图片 load/error 完成。
44
+ *
45
+ * 截图 / PDF 导出前必须调用:CanvasBackend(以及 image-paint 填充)只在
46
+ * `img.complete && naturalWidth > 0` 时才 drawImage,否则静默跳过。导出时若不等待图片
47
+ * 解码完成,就会随机丢图——同一份 deck 先后导出 PNG / PDF 结果不一致正是这个原因。
48
+ *
49
+ * 调用前应先让 backend 跑过一次绘制(flush),把所有 src 注册进 imageCache 并开始加载。
50
+ * 每张图带超时兜底,避免坏链 / 慢图把导出永久卡死。
51
+ */
52
+ function waitForImagesLoaded(timeoutMs = 10000) {
53
+ const pending = [];
54
+ for (const img of imageCache.values()) {
55
+ if (img.complete)
56
+ continue;
57
+ pending.push(new Promise((resolve) => {
58
+ let settled = false;
59
+ const done = () => {
60
+ if (settled)
61
+ return;
62
+ settled = true;
63
+ img.removeEventListener("load", done);
64
+ img.removeEventListener("error", done);
65
+ resolve();
66
+ };
67
+ img.addEventListener("load", done, { once: true });
68
+ img.addEventListener("error", done, { once: true });
69
+ setTimeout(done, timeoutMs);
70
+ }));
71
+ }
72
+ return Promise.all(pending).then(async () => {
73
+ await (0, metafile_preview_1.waitForMetafilePreviews)();
74
+ await (0, video_frame_1.waitForVideoFrames)(timeoutMs);
75
+ await (0, model3d_frame_1.waitForModel3DFrames)(timeoutMs);
76
+ });
77
+ }
78
+ /**
79
+ * shapeHint:承载填充的形状轮廓类别,仅影响 gradFill path="shape"(等值线沿轮廓)
80
+ * 的解释——矩形轮廓走精确的"矩形框带"pattern,其它轮廓退回径向近似。
81
+ */
82
+ function paintToCanvasStyle(ctx, paint, bbox, onAsset, shapeHint) {
83
+ if (!paint || paint.kind === "none")
84
+ return "transparent";
85
+ if (paint.kind === "solid")
86
+ return withAlpha(paint.color, paint.alpha ?? 1);
87
+ if (paint.kind === "linear") {
88
+ // OOXML angle: 0 = →, 90 = ↓ (clockwise).
89
+ const a = (paint.angle * Math.PI) / 180;
90
+ const x1 = bbox.x + bbox.w * (0.5 - Math.cos(a) / 2);
91
+ const y1 = bbox.y + bbox.h * (0.5 - Math.sin(a) / 2);
92
+ const x2 = bbox.x + bbox.w * (0.5 + Math.cos(a) / 2);
93
+ const y2 = bbox.y + bbox.h * (0.5 + Math.sin(a) / 2);
94
+ const g = ctx.createLinearGradient(x1, y1, x2, y2);
95
+ for (const s of safeStops(paint.stops))
96
+ g.addColorStop(s.offset, withAlpha(s.color, s.alpha ?? 1));
97
+ return g;
98
+ }
99
+ if (paint.kind === "radial") {
100
+ // ECMA-376 §20.1.8.30 <a:path path="…">:rect(及 shape+矩形轮廓)的等值线是
101
+ // 嵌套矩形框,Canvas 没有对应渐变原语 → 离屏 canvas 合成框带 pattern。
102
+ const gpath = paint.path ?? "circle";
103
+ const rectLike = gpath === "rect" || (gpath === "shape" && shapeHint === "rect");
104
+ if (rectLike && bbox.w > 0 && bbox.h > 0) {
105
+ const pat = rectFramePattern(ctx, paint, bbox);
106
+ if (pat)
107
+ return pat;
108
+ }
109
+ // fillToRect = focus rect (ECMA-376 §20.1.8.30). stop 0 sits at its center,
110
+ // stop 1 reaches the shape edge. We use a concentric inner/outer pair where
111
+ // r1 = max distance from focus to any bbox corner.
112
+ const { l, t, r, b } = paint.fillToRect;
113
+ const fxN = (l + (1 - r)) / 2;
114
+ const fyN = (t + (1 - b)) / 2;
115
+ const fx = bbox.x + bbox.w * fxN;
116
+ const fy = bbox.y + bbox.h * fyN;
117
+ const rad = Math.max(Math.hypot(bbox.w * fxN, bbox.h * fyN), Math.hypot(bbox.w * (1 - fxN), bbox.h * fyN), Math.hypot(bbox.w * fxN, bbox.h * (1 - fyN)), Math.hypot(bbox.w * (1 - fxN), bbox.h * (1 - fyN)));
118
+ const g = ctx.createRadialGradient(fx, fy, 0, fx, fy, rad || 1);
119
+ for (const s of safeStops(paint.stops))
120
+ g.addColorStop(s.offset, withAlpha(s.color, s.alpha ?? 1));
121
+ return g;
122
+ }
123
+ if (paint.kind === "pattern") {
124
+ const tile = renderPatternTile(paint);
125
+ const pat = ctx.createPattern(tile, "repeat");
126
+ if (pat) {
127
+ const m = new DOMMatrix();
128
+ m.translateSelf(bbox.x, bbox.y);
129
+ pat.setTransform(m);
130
+ return pat;
131
+ }
132
+ return paint.bg ?? "transparent";
133
+ }
134
+ if (paint.kind === "groupFill") {
135
+ return "transparent";
136
+ }
137
+ if (paint.kind === "image") {
138
+ const probe = getImage(paint.src, onAsset);
139
+ const shown = (0, metafile_preview_1.previewImageSrc)(paint.src, probe, onAsset);
140
+ const img = !shown ? probe : shown === paint.src ? probe : getImage(shown, onAsset);
141
+ if (shown && img.complete && img.naturalWidth > 0) {
142
+ const adjusted = paint.adjust ? adjustImage(img, paint.adjust) : img;
143
+ const naturalW = paint.adjust ? adjusted.width : img.naturalWidth;
144
+ const naturalH = paint.adjust ? adjusted.height : img.naturalHeight;
145
+ const repeat = paint.fit === "tile" ? "repeat" : "no-repeat";
146
+ const pat = ctx.createPattern(adjusted, repeat);
147
+ if (pat) {
148
+ const m = new DOMMatrix();
149
+ m.translateSelf(bbox.x, bbox.y);
150
+ if (paint.fit === "tile") {
151
+ const sx = paint.tileSx ?? 1;
152
+ const sy = paint.tileSy ?? 1;
153
+ m.scaleSelf(sx, sy);
154
+ }
155
+ else if (paint.srcRect) {
156
+ const sr = paint.srcRect;
157
+ const cropW = Math.max(0.0001, 1 - sr.l - sr.r);
158
+ const cropH = Math.max(0.0001, 1 - sr.t - sr.b);
159
+ m.translateSelf(-bbox.w * sr.l / cropW, -bbox.h * sr.t / cropH);
160
+ m.scaleSelf(bbox.w / (naturalW * cropW), bbox.h / (naturalH * cropH));
161
+ }
162
+ else if (paint.fit === "cover" || paint.fit === "contain") {
163
+ // 等比缩放:cover 取较大比例(铺满并裁切),contain 取较小比例(完整留白),
164
+ // 再居中。与 SVG 后端 preserveAspectRatio slice/meet 对齐。
165
+ const scale = paint.fit === "cover"
166
+ ? Math.max(bbox.w / naturalW, bbox.h / naturalH)
167
+ : Math.min(bbox.w / naturalW, bbox.h / naturalH);
168
+ m.translateSelf((bbox.w - naturalW * scale) / 2, (bbox.h - naturalH * scale) / 2);
169
+ m.scaleSelf(scale, scale);
170
+ }
171
+ else {
172
+ // fill = 拉伸铺满(OOXML stretch)。
173
+ m.scaleSelf(bbox.w / naturalW, bbox.h / naturalH);
174
+ }
175
+ pat.setTransform(m);
176
+ return pat;
177
+ }
178
+ }
179
+ return "transparent";
180
+ }
181
+ return "transparent";
182
+ }
183
+ const adjustCache = new Map();
184
+ const adjustDataUrlCache = new WeakMap();
185
+ function adjustImage(img, adj) {
186
+ const key = `${img.src}|${JSON.stringify(adj)}`;
187
+ const cached = adjustCache.get(key);
188
+ if (cached)
189
+ return cached;
190
+ const c = document.createElement("canvas");
191
+ c.width = img.naturalWidth;
192
+ c.height = img.naturalHeight;
193
+ const cx = c.getContext("2d", { willReadFrequently: true });
194
+ if (!cx)
195
+ return c;
196
+ cx.drawImage(img, 0, 0);
197
+ try {
198
+ const data = cx.getImageData(0, 0, c.width, c.height);
199
+ applyAdjust(data.data, adj);
200
+ if (adj.blur && adj.blur.radius > 0.2) {
201
+ boxBlur(data.data, c.width, c.height, adj.blur.radius);
202
+ }
203
+ cx.putImageData(data, 0, 0);
204
+ }
205
+ catch {
206
+ // Cross-origin tainted canvas; bail with un-adjusted image.
207
+ }
208
+ adjustCache.set(key, c);
209
+ return c;
210
+ }
211
+ /** 图片已加载时返回调色后的 data URL,供 SVG `<image href>` 使用。 */
212
+ function rasterizedAdjustHref(src, adj, onLoad) {
213
+ const img = getImage(src, onLoad);
214
+ if (!img.complete || img.naturalWidth <= 0)
215
+ return undefined;
216
+ try {
217
+ const c = adjustImage(img, adj);
218
+ let url = adjustDataUrlCache.get(c);
219
+ if (!url) {
220
+ url = c.toDataURL();
221
+ adjustDataUrlCache.set(c, url);
222
+ }
223
+ return url;
224
+ }
225
+ catch {
226
+ return undefined;
227
+ }
228
+ }
229
+ function boxBlur(buf, w, h, radius) {
230
+ const r = Math.max(1, Math.min(24, Math.round(radius)));
231
+ const tmp = new Uint8ClampedArray(buf.length);
232
+ const pass = (src, dst, horiz) => {
233
+ for (let y = 0; y < h; y++) {
234
+ for (let x = 0; x < w; x++) {
235
+ let rs = 0;
236
+ let gs = 0;
237
+ let bs = 0;
238
+ let as = 0;
239
+ let n = 0;
240
+ for (let k = -r; k <= r; k++) {
241
+ const xx = horiz ? Math.min(w - 1, Math.max(0, x + k)) : x;
242
+ const yy = horiz ? y : Math.min(h - 1, Math.max(0, y + k));
243
+ const i = (yy * w + xx) * 4;
244
+ rs += src[i];
245
+ gs += src[i + 1];
246
+ bs += src[i + 2];
247
+ as += src[i + 3];
248
+ n++;
249
+ }
250
+ const i = (y * w + x) * 4;
251
+ dst[i] = Math.round(rs / n);
252
+ dst[i + 1] = Math.round(gs / n);
253
+ dst[i + 2] = Math.round(bs / n);
254
+ dst[i + 3] = Math.round(as / n);
255
+ }
256
+ }
257
+ };
258
+ pass(buf, tmp, true);
259
+ pass(tmp, buf, false);
260
+ }
261
+ function applyAdjust(buf, adj) {
262
+ const { grayscale, lum, hsl, duotone, alphaMod, biLevel, clrChange, fillOverlay } = adj;
263
+ const duoFrom = duotone ? parseHexRGB(duotone[0]) : null;
264
+ const duoTo = duotone ? parseHexRGB(duotone[1]) : null;
265
+ const changeFrom = clrChange ? parseHexRGB(clrChange.from) : null;
266
+ const changeTo = clrChange ? parseHexRGB(clrChange.to) : null;
267
+ const overlay = fillOverlay ? parseHexRGB(fillOverlay.color) : null;
268
+ for (let i = 0; i < buf.length; i += 4) {
269
+ let r = buf[i] / 255;
270
+ let g = buf[i + 1] / 255;
271
+ let b = buf[i + 2] / 255;
272
+ let a = buf[i + 3] / 255;
273
+ if (grayscale || duotone) {
274
+ const y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
275
+ r = g = b = y;
276
+ }
277
+ if (lum) {
278
+ const c = lum.contrast ?? 0;
279
+ const br = lum.bright ?? 0;
280
+ const slope = 1 + c;
281
+ const intercept = br - c / 2;
282
+ r = slope * r + intercept;
283
+ g = slope * g + intercept;
284
+ b = slope * b + intercept;
285
+ }
286
+ if (hsl) {
287
+ const [h, s, l] = rgbToHslLocal(r, g, b);
288
+ const nh = (h + ((hsl.hue ?? 0) / 360) + 1) % 1;
289
+ const ns = clamp01Local(s + (hsl.sat ?? 0));
290
+ const nl = clamp01Local(l + (hsl.lum ?? 0));
291
+ [r, g, b] = hslToRgbLocal(nh, ns, nl);
292
+ }
293
+ if (duoFrom && duoTo) {
294
+ const y = r;
295
+ r = duoFrom[0] + (duoTo[0] - duoFrom[0]) * y;
296
+ g = duoFrom[1] + (duoTo[1] - duoFrom[1]) * y;
297
+ b = duoFrom[2] + (duoTo[2] - duoFrom[2]) * y;
298
+ }
299
+ if (biLevel !== undefined) {
300
+ const y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
301
+ const v = y >= biLevel ? 1 : 0;
302
+ r = g = b = v;
303
+ }
304
+ if (changeFrom && changeTo) {
305
+ const dist = Math.abs(r - changeFrom[0]) + Math.abs(g - changeFrom[1]) + Math.abs(b - changeFrom[2]);
306
+ const alphaOk = !clrChange?.useA || a > 0.02;
307
+ if (alphaOk && dist < 0.36) {
308
+ r = changeTo[0];
309
+ g = changeTo[1];
310
+ b = changeTo[2];
311
+ }
312
+ }
313
+ if (overlay) {
314
+ const blend = fillOverlay?.blend ?? "over";
315
+ if (blend === "mult") {
316
+ r *= overlay[0];
317
+ g *= overlay[1];
318
+ b *= overlay[2];
319
+ }
320
+ else if (blend === "screen") {
321
+ r = 1 - (1 - r) * (1 - overlay[0]);
322
+ g = 1 - (1 - g) * (1 - overlay[1]);
323
+ b = 1 - (1 - b) * (1 - overlay[2]);
324
+ }
325
+ else if (blend === "darken") {
326
+ r = Math.min(r, overlay[0]);
327
+ g = Math.min(g, overlay[1]);
328
+ b = Math.min(b, overlay[2]);
329
+ }
330
+ else if (blend === "lighten") {
331
+ r = Math.max(r, overlay[0]);
332
+ g = Math.max(g, overlay[1]);
333
+ b = Math.max(b, overlay[2]);
334
+ }
335
+ else {
336
+ const oa = 0.45;
337
+ r = overlay[0] * oa + r * (1 - oa);
338
+ g = overlay[1] * oa + g * (1 - oa);
339
+ b = overlay[2] * oa + b * (1 - oa);
340
+ }
341
+ }
342
+ if (alphaMod !== undefined)
343
+ a = a * alphaMod;
344
+ if (adj.clrRepl) {
345
+ const repl = parseHexRGB(adj.clrRepl);
346
+ r = repl[0];
347
+ g = repl[1];
348
+ b = repl[2];
349
+ }
350
+ if (adj.tint) {
351
+ const [h, s, l] = rgbToHslLocal(r, g, b);
352
+ const hueDeg = adj.tint.hue ?? h * 360;
353
+ const amt = clamp01Local(adj.tint.amount ?? 0);
354
+ const th = ((hueDeg / 360) % 1 + 1) % 1;
355
+ let dh = th - h;
356
+ if (dh > 0.5)
357
+ dh -= 1;
358
+ if (dh < -0.5)
359
+ dh += 1;
360
+ const nh = (h + dh * amt + 1) % 1;
361
+ const ns = s * (1 - amt) + amt;
362
+ [r, g, b] = hslToRgbLocal(nh, ns, l);
363
+ }
364
+ if (adj.alphaReplace !== undefined)
365
+ a = adj.alphaReplace;
366
+ if (adj.alphaBiLevel !== undefined)
367
+ a = a >= adj.alphaBiLevel ? 1 : 0;
368
+ if (adj.alphaCeiling)
369
+ a = a > 0 ? 1 : 0;
370
+ if (adj.alphaFloor)
371
+ a = a < 1 ? 0 : a;
372
+ buf[i] = Math.round(clamp01Local(r) * 255);
373
+ buf[i + 1] = Math.round(clamp01Local(g) * 255);
374
+ buf[i + 2] = Math.round(clamp01Local(b) * 255);
375
+ buf[i + 3] = Math.round(clamp01Local(a) * 255);
376
+ }
377
+ }
378
+ function clamp01Local(v) {
379
+ return v < 0 ? 0 : v > 1 ? 1 : v;
380
+ }
381
+ function parseHexRGB(c) {
382
+ const m = /^#?([0-9a-f]{6})$/i.exec(c);
383
+ if (!m)
384
+ return [0, 0, 0];
385
+ const v = m[1];
386
+ return [
387
+ parseInt(v.slice(0, 2), 16) / 255,
388
+ parseInt(v.slice(2, 4), 16) / 255,
389
+ parseInt(v.slice(4, 6), 16) / 255,
390
+ ];
391
+ }
392
+ function rgbToHslLocal(r, g, b) {
393
+ const max = Math.max(r, g, b);
394
+ const min = Math.min(r, g, b);
395
+ let h = 0;
396
+ let s = 0;
397
+ const l = (max + min) / 2;
398
+ if (max !== min) {
399
+ const d = max - min;
400
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
401
+ if (max === r)
402
+ h = (g - b) / d + (g < b ? 6 : 0);
403
+ else if (max === g)
404
+ h = (b - r) / d + 2;
405
+ else
406
+ h = (r - g) / d + 4;
407
+ h /= 6;
408
+ }
409
+ return [h, s, l];
410
+ }
411
+ function hslToRgbLocal(h, s, l) {
412
+ if (s === 0)
413
+ return [l, l, l];
414
+ const hue2rgb = (p, q, t) => {
415
+ let tt = t;
416
+ if (tt < 0)
417
+ tt += 1;
418
+ if (tt > 1)
419
+ tt -= 1;
420
+ if (tt < 1 / 6)
421
+ return p + (q - p) * 6 * tt;
422
+ if (tt < 1 / 2)
423
+ return q;
424
+ if (tt < 2 / 3)
425
+ return p + (q - p) * (2 / 3 - tt) * 6;
426
+ return p;
427
+ };
428
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
429
+ const p = 2 * l - q;
430
+ return [hue2rgb(p, q, h + 1 / 3), hue2rgb(p, q, h), hue2rgb(p, q, h - 1 / 3)];
431
+ }
432
+ function applyStroke(ctx, stroke, bbox, onAsset) {
433
+ if (!stroke)
434
+ return false;
435
+ const style = paintToCanvasStyle(ctx, stroke.paint, bbox, onAsset);
436
+ ctx.strokeStyle = style;
437
+ ctx.lineWidth = stroke.width;
438
+ if (stroke.dash)
439
+ ctx.setLineDash(stroke.dash);
440
+ else
441
+ ctx.setLineDash([]);
442
+ if (stroke.cap)
443
+ ctx.lineCap = stroke.cap;
444
+ if (stroke.join)
445
+ ctx.lineJoin = stroke.join;
446
+ return true;
447
+ }
448
+ // ECMA-376 §20.1.8.47 pattFill — 用 8x8 离屏画布渲染 PowerPoint 预设图案。
449
+ // gradFill path="rect":离屏合成"矩形框带"渐变——中心 fillToRect 铺 stop0 纯色,
450
+ // 四周 4 个梯形各铺一条线性渐变(上下沿 y、左右沿 x)。任一点的框带参数沿 x 量
451
+ // 与沿 y 量在梯形对角接缝上相等 → 四块在接缝处颜色严格一致,视觉无缝。
452
+ // 返回 no-repeat pattern 并 setTransform 对齐 bbox;离屏不可用时返回 null,
453
+ // 调用方退回径向近似。
454
+ function rectFramePattern(ctx, paint, bbox) {
455
+ if (typeof document === "undefined")
456
+ return null;
457
+ const w = Math.max(1, Math.round(bbox.w));
458
+ const h = Math.max(1, Math.round(bbox.h));
459
+ const off = document.createElement("canvas");
460
+ off.width = w;
461
+ off.height = h;
462
+ const octx = off.getContext("2d");
463
+ if (!octx)
464
+ return null;
465
+ const clamp01 = (v) => Math.min(Math.max(v, 0), 1);
466
+ const { l, t, r, b } = paint.fillToRect;
467
+ const fx1 = clamp01(l) * w;
468
+ const fy1 = clamp01(t) * h;
469
+ const fx2 = (1 - clamp01(r)) * w;
470
+ const fy2 = (1 - clamp01(b)) * h;
471
+ let cx1 = Math.min(fx1, fx2);
472
+ let cx2 = Math.max(fx1, fx2);
473
+ let cy1 = Math.min(fy1, fy2);
474
+ let cy2 = Math.max(fy1, fy2);
475
+ // fillToRect 缺省 {0,0,0,0}(focus = 整个形状)→ 与旧 radial 分支一致按中心点处理。
476
+ if (cx1 <= 0 && cx2 >= w && cy1 <= 0 && cy2 >= h) {
477
+ cx1 = cx2 = w / 2;
478
+ cy1 = cy2 = h / 2;
479
+ }
480
+ const stops = safeStops(paint.stops);
481
+ const s0 = stops[0];
482
+ if (s0 && cx2 > cx1 && cy2 > cy1) {
483
+ octx.fillStyle = withAlpha(s0.color, s0.alpha ?? 1);
484
+ octx.fillRect(cx1, cy1, cx2 - cx1, cy2 - cy1);
485
+ }
486
+ const quads = [
487
+ { pts: [[0, 0], [w, 0], [cx2, cy1], [cx1, cy1]], x1: 0, y1: cy1, x2: 0, y2: 0 },
488
+ { pts: [[0, h], [w, h], [cx2, cy2], [cx1, cy2]], x1: 0, y1: cy2, x2: 0, y2: h },
489
+ { pts: [[0, 0], [cx1, cy1], [cx1, cy2], [0, h]], x1: cx1, y1: 0, x2: 0, y2: 0 },
490
+ { pts: [[w, 0], [cx2, cy1], [cx2, cy2], [w, h]], x1: cx2, y1: 0, x2: w, y2: 0 },
491
+ ];
492
+ for (const q of quads) {
493
+ // 渐变轴长度为 0(focus 边贴外缘)→ 该侧无框带,跳过。
494
+ if (q.x1 === q.x2 && q.y1 === q.y2)
495
+ continue;
496
+ const g = octx.createLinearGradient(q.x1, q.y1, q.x2, q.y2);
497
+ for (const s of stops)
498
+ g.addColorStop(s.offset, withAlpha(s.color, s.alpha ?? 1));
499
+ octx.beginPath();
500
+ q.pts.forEach(([px, py], i) => (i === 0 ? octx.moveTo(px, py) : octx.lineTo(px, py)));
501
+ octx.closePath();
502
+ octx.fillStyle = g;
503
+ octx.fill();
504
+ }
505
+ const pat = ctx.createPattern(off, "no-repeat");
506
+ if (!pat)
507
+ return null;
508
+ const m = new DOMMatrix();
509
+ m.translateSelf(bbox.x, bbox.y);
510
+ m.scaleSelf(bbox.w / w, bbox.h / h);
511
+ pat.setTransform(m);
512
+ return pat;
513
+ }
514
+ // 缓存按 (prst, fg, bg, alpha) 维度去重,避免每次重绘都创建。
515
+ const patternTileCache = new Map();
516
+ function renderPatternTile(paint) {
517
+ const key = `${paint.prst}|${paint.fg}|${paint.bg}|${paint.fgAlpha ?? 1}|${paint.bgAlpha ?? 1}`;
518
+ const cached = patternTileCache.get(key);
519
+ if (cached)
520
+ return cached;
521
+ const c = document.createElement("canvas");
522
+ c.width = 8;
523
+ c.height = 8;
524
+ const cx = c.getContext("2d");
525
+ if (!cx)
526
+ return c;
527
+ cx.fillStyle = withAlpha(paint.bg, paint.bgAlpha ?? 1);
528
+ cx.fillRect(0, 0, 8, 8);
529
+ cx.fillStyle = withAlpha(paint.fg, paint.fgAlpha ?? 1);
530
+ cx.strokeStyle = withAlpha(paint.fg, paint.fgAlpha ?? 1);
531
+ cx.lineWidth = 1;
532
+ paintPresetPattern(cx, paint.prst);
533
+ patternTileCache.set(key, c);
534
+ return c;
535
+ }
536
+ function paintPresetPattern(cx, prst) {
537
+ const dot = (x, y, s = 1) => cx.fillRect(x, y, s, s);
538
+ const ln = (x1, y1, x2, y2, w = 1) => {
539
+ const old = cx.lineWidth;
540
+ cx.lineWidth = w;
541
+ cx.beginPath();
542
+ cx.moveTo(x1, y1);
543
+ cx.lineTo(x2, y2);
544
+ cx.stroke();
545
+ cx.lineWidth = old;
546
+ };
547
+ switch (prst) {
548
+ case "pct5":
549
+ dot(0, 0);
550
+ break;
551
+ case "pct10":
552
+ dot(0, 0);
553
+ dot(4, 4);
554
+ break;
555
+ case "pct20":
556
+ dot(0, 0);
557
+ dot(2, 2);
558
+ dot(4, 4);
559
+ dot(6, 6);
560
+ break;
561
+ case "pct25":
562
+ dot(0, 0);
563
+ dot(2, 2);
564
+ dot(4, 4);
565
+ dot(6, 6);
566
+ dot(0, 4);
567
+ dot(4, 0);
568
+ break;
569
+ case "pct30":
570
+ case "pct40":
571
+ case "pct50":
572
+ dot(0, 0);
573
+ dot(2, 2);
574
+ dot(4, 4);
575
+ dot(6, 6);
576
+ dot(0, 4);
577
+ dot(2, 6);
578
+ dot(4, 0);
579
+ dot(6, 2);
580
+ break;
581
+ case "pct60":
582
+ case "pct70":
583
+ case "pct75":
584
+ case "pct80":
585
+ case "pct90":
586
+ cx.fillRect(0, 0, 8, 8);
587
+ break;
588
+ case "horz":
589
+ ln(0, 4, 8, 4);
590
+ break;
591
+ case "vert":
592
+ ln(4, 0, 4, 8);
593
+ break;
594
+ case "ltHorz":
595
+ ln(0, 2, 8, 2);
596
+ ln(0, 6, 8, 6);
597
+ break;
598
+ case "ltVert":
599
+ ln(2, 0, 2, 8);
600
+ ln(6, 0, 6, 8);
601
+ break;
602
+ case "dkHorz":
603
+ ln(0, 4, 8, 4, 2);
604
+ break;
605
+ case "dkVert":
606
+ ln(4, 0, 4, 8, 2);
607
+ break;
608
+ case "narHorz":
609
+ ln(0, 1, 8, 1);
610
+ ln(0, 3, 8, 3);
611
+ ln(0, 5, 8, 5);
612
+ ln(0, 7, 8, 7);
613
+ break;
614
+ case "narVert":
615
+ ln(1, 0, 1, 8);
616
+ ln(3, 0, 3, 8);
617
+ ln(5, 0, 5, 8);
618
+ ln(7, 0, 7, 8);
619
+ break;
620
+ case "dashHorz":
621
+ ln(0, 4, 4, 4);
622
+ ln(5, 4, 8, 4);
623
+ break;
624
+ case "dashVert":
625
+ ln(4, 0, 4, 4);
626
+ ln(4, 5, 4, 8);
627
+ break;
628
+ case "cross":
629
+ ln(0, 4, 8, 4);
630
+ ln(4, 0, 4, 8);
631
+ break;
632
+ case "dnDiag":
633
+ case "ltDnDiag":
634
+ ln(0, 0, 8, 8);
635
+ ln(-2, 6, 2, 10);
636
+ ln(6, -2, 10, 2);
637
+ break;
638
+ case "upDiag":
639
+ case "ltUpDiag":
640
+ ln(0, 8, 8, 0);
641
+ ln(-2, 2, 2, -2);
642
+ ln(6, 10, 10, 6);
643
+ break;
644
+ case "dkDnDiag":
645
+ ln(0, 0, 8, 8, 2);
646
+ break;
647
+ case "dkUpDiag":
648
+ ln(0, 8, 8, 0, 2);
649
+ break;
650
+ case "wdDnDiag":
651
+ ln(-2, 4, 4, 10);
652
+ ln(4, -2, 10, 4);
653
+ break;
654
+ case "wdUpDiag":
655
+ ln(-2, 4, 4, -2);
656
+ ln(4, 10, 10, 4);
657
+ break;
658
+ case "diagCross":
659
+ ln(0, 0, 8, 8);
660
+ ln(0, 8, 8, 0);
661
+ break;
662
+ case "weave":
663
+ case "trellis":
664
+ case "smGrid":
665
+ case "lgGrid":
666
+ ln(0, 4, 8, 4);
667
+ ln(4, 0, 4, 8);
668
+ break;
669
+ case "smCheck":
670
+ case "lgCheck":
671
+ cx.fillRect(0, 0, 4, 4);
672
+ cx.fillRect(4, 4, 4, 4);
673
+ break;
674
+ default:
675
+ dot(0, 0);
676
+ dot(2, 2);
677
+ dot(4, 4);
678
+ dot(6, 6);
679
+ break;
680
+ }
681
+ }
682
+ function withAlpha(color, a) {
683
+ if (a >= 1)
684
+ return color;
685
+ if (color.startsWith("#")) {
686
+ const h = color.slice(1);
687
+ let r = 0, g = 0, b = 0;
688
+ if (h.length === 3) {
689
+ r = parseInt(h[0] + h[0], 16);
690
+ g = parseInt(h[1] + h[1], 16);
691
+ b = parseInt(h[2] + h[2], 16);
692
+ }
693
+ else {
694
+ r = parseInt(h.slice(0, 2), 16);
695
+ g = parseInt(h.slice(2, 4), 16);
696
+ b = parseInt(h.slice(4, 6), 16);
697
+ }
698
+ return `rgba(${r},${g},${b},${a})`;
699
+ }
700
+ return color;
701
+ }