pptx-browser 4.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.
- package/LICENSE +9 -0
- package/README.md +209 -0
- package/package.json +53 -0
- package/src/animation.js +817 -0
- package/src/charts.js +989 -0
- package/src/clipboard.js +416 -0
- package/src/colors.js +297 -0
- package/src/effects3d.js +312 -0
- package/src/extract.js +535 -0
- package/src/fntdata.js +265 -0
- package/src/fonts.js +676 -0
- package/src/index.js +751 -0
- package/src/pdf.js +298 -0
- package/src/render.js +1964 -0
- package/src/shapes.js +666 -0
- package/src/slideshow.js +492 -0
- package/src/smartart.js +696 -0
- package/src/svg.js +732 -0
- package/src/theme.js +88 -0
- package/src/utils.js +50 -0
- package/src/writer.js +1015 -0
- package/src/zip-writer.js +214 -0
- package/src/zip.js +194 -0
package/src/colors.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* colors.js — OOXML colour resolution: all 6 colour types, all transforms.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { attrInt, clamp } from './utils.js';
|
|
6
|
+
|
|
7
|
+
// ── Hex / RGB / HLS conversion ──────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export function hexToRgb(hex) {
|
|
10
|
+
const h = hex.replace('#', '');
|
|
11
|
+
return {
|
|
12
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
13
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
14
|
+
b: parseInt(h.slice(4, 6), 16),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function rgbToHex(r, g, b) {
|
|
19
|
+
return '#' + [r, g, b]
|
|
20
|
+
.map(v => clamp(Math.round(v), 0, 255).toString(16).padStart(2, '0'))
|
|
21
|
+
.join('');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function rgbToHls(r, g, b) {
|
|
25
|
+
r /= 255; g /= 255; b /= 255;
|
|
26
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
27
|
+
let h = 0, s = 0;
|
|
28
|
+
const l = (max + min) / 2;
|
|
29
|
+
if (max !== min) {
|
|
30
|
+
const d = max - min;
|
|
31
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
32
|
+
switch (max) {
|
|
33
|
+
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
|
34
|
+
case g: h = ((b - r) / d + 2) / 6; break;
|
|
35
|
+
case b: h = ((r - g) / d + 4) / 6; break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { h, l, s };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function hlsToRgb(h, l, s) {
|
|
42
|
+
if (s === 0) {
|
|
43
|
+
const v = Math.round(l * 255);
|
|
44
|
+
return { r: v, g: v, b: v };
|
|
45
|
+
}
|
|
46
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
47
|
+
const p = 2 * l - q;
|
|
48
|
+
const hue2rgb = (p, q, t) => {
|
|
49
|
+
if (t < 0) t += 1;
|
|
50
|
+
if (t > 1) t -= 1;
|
|
51
|
+
if (t < 1/6) return p + (q - p) * 6 * t;
|
|
52
|
+
if (t < 1/2) return q;
|
|
53
|
+
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
|
54
|
+
return p;
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
r: Math.round(hue2rgb(p, q, h + 1/3) * 255),
|
|
58
|
+
g: Math.round(hue2rgb(p, q, h) * 255),
|
|
59
|
+
b: Math.round(hue2rgb(p, q, h - 1/3) * 255),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Preset colours (CSS colour names → hex) ─────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const PRESET_COLORS = {
|
|
66
|
+
black:'000000', white:'FFFFFF', red:'FF0000', green:'008000', blue:'0000FF',
|
|
67
|
+
yellow:'FFFF00', cyan:'00FFFF', magenta:'FF00FF', orange:'FFA500',
|
|
68
|
+
purple:'800080', pink:'FFC0CB', brown:'A52A2A', gray:'808080', grey:'808080',
|
|
69
|
+
navy:'000080', teal:'008080', maroon:'800000', olive:'808000', lime:'00FF00',
|
|
70
|
+
aqua:'00FFFF', fuchsia:'FF00FF', silver:'C0C0C0', coral:'FF7F50',
|
|
71
|
+
salmon:'FA8072', gold:'FFD700', khaki:'F0E68C', lavender:'E6E6FA',
|
|
72
|
+
beige:'F5F5DC', ivory:'FFFFF0', mintcream:'F5FFFA', azure:'F0FFFF',
|
|
73
|
+
aliceblue:'F0F8FF', ghostwhite:'F8F8FF', darkred:'8B0000',
|
|
74
|
+
darkgreen:'006400', darkblue:'00008B', darkcyan:'008B8B',
|
|
75
|
+
darkmagenta:'8B008B', darkorange:'FF8C00', darkgray:'A9A9A9',
|
|
76
|
+
darkgrey:'A9A9A9', lightgray:'D3D3D3', lightgrey:'D3D3D3',
|
|
77
|
+
lightblue:'ADD8E6', lightgreen:'90EE90', lightpink:'FFB6C1',
|
|
78
|
+
lightyellow:'FFFFE0', lightcyan:'E0FFFF', deepskyblue:'00BFFF',
|
|
79
|
+
royalblue:'4169E1', steelblue:'4682B4', skyblue:'87CEEB',
|
|
80
|
+
dodgerblue:'1E90FF', cornflowerblue:'6495ED', mediumblue:'0000CD',
|
|
81
|
+
midnightblue:'191970', indigo:'4B0082', slateblue:'6A5ACD',
|
|
82
|
+
blueviolet:'8A2BE2', mediumpurple:'9370DB', orchid:'DA70D6',
|
|
83
|
+
violet:'EE82EE', plum:'DDA0DD', thistle:'D8BFD8', hotpink:'FF69B4',
|
|
84
|
+
deeppink:'FF1493', crimson:'DC143C', firebrick:'B22222', tomato:'FF6347',
|
|
85
|
+
orangered:'FF4500', darkorange2:'FF8C00', chocolate:'D2691E',
|
|
86
|
+
saddlebrown:'8B4513', sienna:'A0522D', tan:'D2B48C', burlywood:'DEB887',
|
|
87
|
+
wheat:'F5DEB3', moccasin:'FFE4B5', peachpuff:'FFDAB9', papayawhip:'FFEFD5',
|
|
88
|
+
mistyrose:'FFE4E1', linen:'FAF0E6', oldlace:'FDF5E6', floralwhite:'FFFAF0',
|
|
89
|
+
antiquewhite:'FAEBD7', bisque:'FFE4C4', blanchedalmond:'FFEBCD',
|
|
90
|
+
cornsilk:'FFF8DC', lemonchiffon:'FFFACD', honeydew:'F0FFF0',
|
|
91
|
+
palegreen:'98FB98', lightseagreen:'20B2AA', mediumseagreen:'3CB371',
|
|
92
|
+
seagreen:'2E8B57', forestgreen:'228B22', yellowgreen:'9ACD32',
|
|
93
|
+
olivedrab:'6B8E23', greenyellow:'ADFF2F', chartreuse:'7FFF00',
|
|
94
|
+
springgreen:'00FF7F', mediumspringgreen:'00FA9A', aquamarine:'7FFFD4',
|
|
95
|
+
turquoise:'40E0D0', mediumturquoise:'48D1CC', paleturquoise:'AFEEEE',
|
|
96
|
+
cadetblue:'5F9EA0', powderblue:'B0E0E6', lightsteelblue:'B0C4DE',
|
|
97
|
+
slategray:'708090', slategrey:'708090', dimgray:'696969', dimgrey:'696969',
|
|
98
|
+
snow:'FFFAFA', seashell:'FFF5EE', whitesmoke:'F5F5F5', gainsboro:'DCDCDC',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ── Colour transforms ────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Apply all OOXML colour transforms in document order.
|
|
105
|
+
* `c` = { r, g, b, a }; returns the same shape.
|
|
106
|
+
*/
|
|
107
|
+
export function applyColorTransforms(c, transformEl) {
|
|
108
|
+
if (!transformEl) return c;
|
|
109
|
+
let { r, g, b, a = 1 } = c;
|
|
110
|
+
|
|
111
|
+
for (const child of transformEl.children) {
|
|
112
|
+
const ln = child.localName;
|
|
113
|
+
const val = parseInt(child.getAttribute('val') ?? '0', 10);
|
|
114
|
+
|
|
115
|
+
switch (ln) {
|
|
116
|
+
case 'lumMod': {
|
|
117
|
+
// val in 1/1000th of a percent (100000 = 100%)
|
|
118
|
+
const f = val / 100000;
|
|
119
|
+
const hls = rgbToHls(r, g, b);
|
|
120
|
+
const rgb = hlsToRgb(hls.h, clamp(hls.l * f, 0, 1), hls.s);
|
|
121
|
+
r = rgb.r; g = rgb.g; b = rgb.b;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 'lumOff': {
|
|
125
|
+
const f = val / 100000;
|
|
126
|
+
const hls = rgbToHls(r, g, b);
|
|
127
|
+
const rgb = hlsToRgb(hls.h, clamp(hls.l + f, 0, 1), hls.s);
|
|
128
|
+
r = rgb.r; g = rgb.g; b = rgb.b;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'tint': {
|
|
132
|
+
// Blend toward white
|
|
133
|
+
const f = val / 100000;
|
|
134
|
+
r = Math.round(r + (255 - r) * (1 - f));
|
|
135
|
+
g = Math.round(g + (255 - g) * (1 - f));
|
|
136
|
+
b = Math.round(b + (255 - b) * (1 - f));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'shade': {
|
|
140
|
+
// Blend toward black
|
|
141
|
+
const f = val / 100000;
|
|
142
|
+
r = Math.round(r * f);
|
|
143
|
+
g = Math.round(g * f);
|
|
144
|
+
b = Math.round(b * f);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'satMod': {
|
|
148
|
+
const f = val / 100000;
|
|
149
|
+
const hls = rgbToHls(r, g, b);
|
|
150
|
+
const rgb = hlsToRgb(hls.h, hls.l, clamp(hls.s * f, 0, 1));
|
|
151
|
+
r = rgb.r; g = rgb.g; b = rgb.b;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
case 'satOff': {
|
|
155
|
+
const f = val / 100000;
|
|
156
|
+
const hls = rgbToHls(r, g, b);
|
|
157
|
+
const rgb = hlsToRgb(hls.h, hls.l, clamp(hls.s + f, 0, 1));
|
|
158
|
+
r = rgb.r; g = rgb.g; b = rgb.b;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'hueMod': {
|
|
162
|
+
const f = val / 100000;
|
|
163
|
+
const hls = rgbToHls(r, g, b);
|
|
164
|
+
const rgb = hlsToRgb((hls.h * f) % 1, hls.l, hls.s);
|
|
165
|
+
r = rgb.r; g = rgb.g; b = rgb.b;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'hueOff': {
|
|
169
|
+
const f = val / 21600000; // 60000ths of a degree, full circle = 21600000
|
|
170
|
+
const hls = rgbToHls(r, g, b);
|
|
171
|
+
const rgb = hlsToRgb(((hls.h + f) % 1 + 1) % 1, hls.l, hls.s);
|
|
172
|
+
r = rgb.r; g = rgb.g; b = rgb.b;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'alpha': {
|
|
176
|
+
a = val / 100000;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'alphaOff': {
|
|
180
|
+
a = clamp(a + val / 100000, 0, 1);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case 'alphaMod': {
|
|
184
|
+
a = clamp(a * val / 100000, 0, 1);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case 'inv': {
|
|
188
|
+
r = 255 - r; g = 255 - g; b = 255 - b;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'gray': {
|
|
192
|
+
const lum = Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b);
|
|
193
|
+
r = g = b = lum;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case 'comp': {
|
|
197
|
+
const hls = rgbToHls(r, g, b);
|
|
198
|
+
const rgb = hlsToRgb((hls.h + 0.5) % 1, hls.l, hls.s);
|
|
199
|
+
r = rgb.r; g = rgb.g; b = rgb.b;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { r: clamp(r, 0, 255), g: clamp(g, 0, 255), b: clamp(b, 0, 255), a };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Resolve an OOXML colour element to { r, g, b, a } */
|
|
209
|
+
export function resolveColorElement(colorEl, themeColors) {
|
|
210
|
+
if (!colorEl) return null;
|
|
211
|
+
const ln = colorEl.localName;
|
|
212
|
+
|
|
213
|
+
let rgb = null;
|
|
214
|
+
let a = 1;
|
|
215
|
+
|
|
216
|
+
if (ln === 'srgbClr') {
|
|
217
|
+
const val = colorEl.getAttribute('val') || '';
|
|
218
|
+
if (val.length >= 6) rgb = hexToRgb(val);
|
|
219
|
+
} else if (ln === 'schemeClr') {
|
|
220
|
+
const schemeVal = colorEl.getAttribute('val') || '';
|
|
221
|
+
// Map scheme names to base colours via themeColors
|
|
222
|
+
const key = schemeVal; // e.g. "dk1", "accent1", "tx1", "bg1" …
|
|
223
|
+
const hex = themeColors?.[key];
|
|
224
|
+
if (hex) rgb = hexToRgb(hex);
|
|
225
|
+
else rgb = { r: 0, g: 0, b: 0 }; // fallback black
|
|
226
|
+
} else if (ln === 'prstClr') {
|
|
227
|
+
const prstVal = (colorEl.getAttribute('val') || '').toLowerCase();
|
|
228
|
+
const hex = PRESET_COLORS[prstVal];
|
|
229
|
+
if (hex) rgb = hexToRgb(hex);
|
|
230
|
+
} else if (ln === 'sysClr') {
|
|
231
|
+
// System colour — use lastClr if available, else fallback
|
|
232
|
+
const lastClr = colorEl.getAttribute('lastClr');
|
|
233
|
+
if (lastClr?.length >= 6) rgb = hexToRgb(lastClr);
|
|
234
|
+
else rgb = { r: 0, g: 0, b: 0 };
|
|
235
|
+
} else if (ln === 'hslClr') {
|
|
236
|
+
const h = attrInt(colorEl, 'hue', 0) / 21600000;
|
|
237
|
+
const s = attrInt(colorEl, 'sat', 0) / 100000;
|
|
238
|
+
const l = attrInt(colorEl, 'lum', 0) / 100000;
|
|
239
|
+
rgb = hlsToRgb(h, l, s);
|
|
240
|
+
} else if (ln === 'scRgbClr') {
|
|
241
|
+
// Linear-light (0–100000)
|
|
242
|
+
const r2 = attrInt(colorEl, 'r', 0) / 100000;
|
|
243
|
+
const g2 = attrInt(colorEl, 'g', 0) / 100000;
|
|
244
|
+
const b2 = attrInt(colorEl, 'b', 0) / 100000;
|
|
245
|
+
rgb = {
|
|
246
|
+
r: Math.round(Math.pow(r2, 1/2.2) * 255),
|
|
247
|
+
g: Math.round(Math.pow(g2, 1/2.2) * 255),
|
|
248
|
+
b: Math.round(Math.pow(b2, 1/2.2) * 255),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!rgb) return null;
|
|
253
|
+
|
|
254
|
+
// Apply any transforms (children of the colour element)
|
|
255
|
+
return applyColorTransforms({ ...rgb, a }, colorEl);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Return a CSS colour string from { r, g, b, a }.
|
|
260
|
+
* @param {{r:number,g:number,b:number,a?:number}} c
|
|
261
|
+
* @param {number} [alphaOverride] Optional alpha (0–1) that overrides c.a
|
|
262
|
+
*/
|
|
263
|
+
export function colorToCss(c, alphaOverride) {
|
|
264
|
+
if (!c) return 'transparent';
|
|
265
|
+
const a = alphaOverride !== undefined ? alphaOverride : (c.a ?? 1);
|
|
266
|
+
return a < 1
|
|
267
|
+
? `rgba(${c.r},${c.g},${c.b},${a.toFixed(3)})`
|
|
268
|
+
: `rgb(${c.r},${c.g},${c.b})`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Find the first recognised colour child element. */
|
|
272
|
+
export function findFirstColorChild(el) {
|
|
273
|
+
if (!el) return null;
|
|
274
|
+
const tags = ['srgbClr','schemeClr','prstClr','sysClr','hslClr','scRgbClr'];
|
|
275
|
+
for (const tag of tags) {
|
|
276
|
+
const child = g1(el, tag);
|
|
277
|
+
if (child) return child;
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Convenience: resolve a run's text colour, returning a CSS string or null. */
|
|
283
|
+
export function getRunColor(rPr, themeColors) {
|
|
284
|
+
if (!rPr) return null;
|
|
285
|
+
const solidFill = g1(rPr, 'solidFill');
|
|
286
|
+
if (!solidFill) return null;
|
|
287
|
+
const colorChild = findFirstColorChild(solidFill);
|
|
288
|
+
const c = resolveColorElement(colorChild, themeColors);
|
|
289
|
+
return c ? colorToCss(c) : null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Resolve run color with paraDefRPr fallback. */
|
|
293
|
+
export function getRunColorInherited(rPr, paraDefRPr, themeColors) {
|
|
294
|
+
const c1 = getRunColor(rPr, themeColors);
|
|
295
|
+
if (c1) return c1;
|
|
296
|
+
return getRunColor(paraDefRPr, themeColors);
|
|
297
|
+
}
|
package/src/effects3d.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* effects3d.js — OOXML 3D effect renderer.
|
|
3
|
+
*
|
|
4
|
+
* Implements sp3d (shape-level 3D) and scene3d (scene-level) effects
|
|
5
|
+
* using canvas 2D gradient tricks to simulate:
|
|
6
|
+
*
|
|
7
|
+
* • Bevel: top/bottom/left/right gradient edges
|
|
8
|
+
* • Extrusion: depth offset shadow-box
|
|
9
|
+
* • Contour: outline with 3D colour
|
|
10
|
+
* • Camera: perspective warp (affine approximation via ctx.transform)
|
|
11
|
+
* • Lighting: diffuse + specular gradient overlay
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* const cleanup = apply3DEffects(ctx, spPr, themeColors, x, y, w, h, scale);
|
|
15
|
+
* // ... draw shape path ...
|
|
16
|
+
* // fill the shape
|
|
17
|
+
* draw3DOverlay(ctx, sp3d, scene3d, themeColors, x, y, w, h, scale);
|
|
18
|
+
* cleanup();
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { g1, attrInt, attr } from './utils.js';
|
|
22
|
+
import { resolveColorElement, findFirstColorChild, colorToCss } from './colors.js';
|
|
23
|
+
|
|
24
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Parse an EMU-valued attribute to canvas pixels. */
|
|
27
|
+
function emu(el, name, def = 0, scale = 1) {
|
|
28
|
+
const v = el ? parseInt(el.getAttribute(name) || def, 10) : def;
|
|
29
|
+
return v * scale;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Lum mod: darken/lighten a CSS hex colour by a factor (0–2). */
|
|
33
|
+
function lumMod(hex, factor) {
|
|
34
|
+
if (!hex || !hex.startsWith('#')) return hex;
|
|
35
|
+
let r = parseInt(hex.slice(1, 3), 16);
|
|
36
|
+
let g = parseInt(hex.slice(3, 5), 16);
|
|
37
|
+
let b = parseInt(hex.slice(5, 7), 16);
|
|
38
|
+
r = Math.round(Math.min(255, r * factor));
|
|
39
|
+
g = Math.round(Math.min(255, g * factor));
|
|
40
|
+
b = Math.round(Math.min(255, b * factor));
|
|
41
|
+
return `rgb(${r},${g},${b})`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hexToCss(hex) {
|
|
45
|
+
return hex ? '#' + hex : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Read a colour element group from sp3d sub-elements like bevelT / extrusionClr. */
|
|
49
|
+
function read3dColor(parentEl, themeColors) {
|
|
50
|
+
if (!parentEl) return null;
|
|
51
|
+
const colorChild = findFirstColorChild(parentEl);
|
|
52
|
+
if (!colorChild) return null;
|
|
53
|
+
const c = resolveColorElement(colorChild, themeColors);
|
|
54
|
+
return c ? colorToCss(c) : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Bevel ─────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Draw a bevel effect (raised or inset edges) over a filled shape.
|
|
61
|
+
*
|
|
62
|
+
* Reads: sp3d > bevelT, bevelB (top and bottom bevel)
|
|
63
|
+
* Simulated with thin gradient strips along each edge.
|
|
64
|
+
*/
|
|
65
|
+
function drawBevel(ctx, sp3d, x, y, w, h, scale) {
|
|
66
|
+
const bevelT = g1(sp3d, 'bevelT');
|
|
67
|
+
const bevelB = g1(sp3d, 'bevelB');
|
|
68
|
+
const bevel = bevelT || bevelB;
|
|
69
|
+
if (!bevel) return;
|
|
70
|
+
|
|
71
|
+
const bw = Math.max(2 * scale, emu(bevel, 'w', 76200, scale));
|
|
72
|
+
const bh = Math.max(2 * scale, emu(bevel, 'h', 76200, scale));
|
|
73
|
+
const prst = attr(bevel, 'prst', 'circle');
|
|
74
|
+
const inset = prst.toLowerCase().includes('in');
|
|
75
|
+
|
|
76
|
+
// Clamp bevel to half shape size
|
|
77
|
+
const bwx = Math.min(bw, w * 0.3);
|
|
78
|
+
const bhy = Math.min(bh, h * 0.3);
|
|
79
|
+
|
|
80
|
+
const lightColor = inset ? 'rgba(0,0,0,0.18)' : 'rgba(255,255,255,0.35)';
|
|
81
|
+
const shadowColor = inset ? 'rgba(255,255,255,0.20)' : 'rgba(0,0,0,0.20)';
|
|
82
|
+
|
|
83
|
+
// Top edge
|
|
84
|
+
const gTop = ctx.createLinearGradient(x, y, x, y + bhy);
|
|
85
|
+
gTop.addColorStop(0, lightColor);
|
|
86
|
+
gTop.addColorStop(1, 'rgba(255,255,255,0)');
|
|
87
|
+
ctx.save();
|
|
88
|
+
ctx.fillStyle = gTop;
|
|
89
|
+
ctx.fillRect(x, y, w, bhy);
|
|
90
|
+
|
|
91
|
+
// Bottom edge
|
|
92
|
+
const gBot = ctx.createLinearGradient(x, y + h - bhy, x, y + h);
|
|
93
|
+
gBot.addColorStop(0, 'rgba(0,0,0,0)');
|
|
94
|
+
gBot.addColorStop(1, shadowColor);
|
|
95
|
+
ctx.fillStyle = gBot;
|
|
96
|
+
ctx.fillRect(x, y + h - bhy, w, bhy);
|
|
97
|
+
|
|
98
|
+
// Left edge
|
|
99
|
+
const gLeft = ctx.createLinearGradient(x, y, x + bwx, y);
|
|
100
|
+
gLeft.addColorStop(0, lightColor);
|
|
101
|
+
gLeft.addColorStop(1, 'rgba(255,255,255,0)');
|
|
102
|
+
ctx.fillStyle = gLeft;
|
|
103
|
+
ctx.fillRect(x, y, bwx, h);
|
|
104
|
+
|
|
105
|
+
// Right edge
|
|
106
|
+
const gRight = ctx.createLinearGradient(x + w - bwx, y, x + w, y);
|
|
107
|
+
gRight.addColorStop(0, 'rgba(0,0,0,0)');
|
|
108
|
+
gRight.addColorStop(1, shadowColor);
|
|
109
|
+
ctx.fillStyle = gRight;
|
|
110
|
+
ctx.fillRect(x + w - bwx, y, bwx, h);
|
|
111
|
+
|
|
112
|
+
ctx.restore();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Extrusion ─────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Draw a 3D extrusion effect — a solid depth offset below/behind the shape.
|
|
119
|
+
*
|
|
120
|
+
* Reads: sp3d extrusionH, extrusionClr
|
|
121
|
+
*/
|
|
122
|
+
function drawExtrusion(ctx, sp3d, themeColors, x, y, w, h, scale) {
|
|
123
|
+
const extH = emu(sp3d, 'extrusionH', 0, scale);
|
|
124
|
+
if (extH < 1 * scale) return;
|
|
125
|
+
|
|
126
|
+
const clrEl = g1(sp3d, 'extrusionClr') || g1(sp3d, 'contourClr');
|
|
127
|
+
const color = read3dColor(clrEl, themeColors) || 'rgba(0,0,0,0.3)';
|
|
128
|
+
|
|
129
|
+
// Depth offset — project slightly down-right
|
|
130
|
+
const depth = Math.min(extH / 914400 * 72 * scale, Math.min(w, h) * 0.15);
|
|
131
|
+
const dx = depth * 0.7;
|
|
132
|
+
const dy = depth * 0.7;
|
|
133
|
+
|
|
134
|
+
ctx.save();
|
|
135
|
+
ctx.fillStyle = color;
|
|
136
|
+
// Right face
|
|
137
|
+
ctx.beginPath();
|
|
138
|
+
ctx.moveTo(x + w, y);
|
|
139
|
+
ctx.lineTo(x + w + dx, y + dy);
|
|
140
|
+
ctx.lineTo(x + w + dx, y + h + dy);
|
|
141
|
+
ctx.lineTo(x + w, y + h);
|
|
142
|
+
ctx.closePath();
|
|
143
|
+
ctx.fill();
|
|
144
|
+
// Bottom face
|
|
145
|
+
ctx.beginPath();
|
|
146
|
+
ctx.moveTo(x, y + h);
|
|
147
|
+
ctx.lineTo(x + dx, y + h + dy);
|
|
148
|
+
ctx.lineTo(x + w + dx, y + h + dy);
|
|
149
|
+
ctx.lineTo(x + w, y + h);
|
|
150
|
+
ctx.closePath();
|
|
151
|
+
ctx.fill();
|
|
152
|
+
ctx.restore();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Contour ───────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
function drawContour(ctx, sp3d, themeColors, x, y, w, h, scale) {
|
|
158
|
+
const contourW = emu(sp3d, 'contourW', 0, scale);
|
|
159
|
+
if (contourW < 0.5) return;
|
|
160
|
+
const clrEl = g1(sp3d, 'contourClr');
|
|
161
|
+
const color = read3dColor(clrEl, themeColors) || '#888888';
|
|
162
|
+
const cw = Math.max(0.5, contourW / 914400 * 72 * scale);
|
|
163
|
+
|
|
164
|
+
ctx.save();
|
|
165
|
+
ctx.strokeStyle = color;
|
|
166
|
+
ctx.lineWidth = cw;
|
|
167
|
+
ctx.strokeRect(x, y, w, h);
|
|
168
|
+
ctx.restore();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Camera / scene3d ──────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Apply a camera perspective transform to the context.
|
|
175
|
+
*
|
|
176
|
+
* scene3d > camera: reads rot (latitude, longitude) and fov
|
|
177
|
+
* Approximated as a simple skew/scale affine transform.
|
|
178
|
+
*
|
|
179
|
+
* Returns a cleanup function that restores the context.
|
|
180
|
+
*/
|
|
181
|
+
export function applyCamera(ctx, scene3d, x, y, w, h) {
|
|
182
|
+
if (!scene3d) return () => {};
|
|
183
|
+
|
|
184
|
+
const camera = g1(scene3d, 'camera');
|
|
185
|
+
if (!camera) return () => {};
|
|
186
|
+
|
|
187
|
+
const prst = attr(camera, 'prst', 'orthographicFront');
|
|
188
|
+
const rot = g1(camera, 'rot');
|
|
189
|
+
|
|
190
|
+
// Only apply non-trivial camera presets
|
|
191
|
+
const isOrtho = prst.toLowerCase().includes('orthographic');
|
|
192
|
+
if (isOrtho && !rot) return () => {};
|
|
193
|
+
|
|
194
|
+
// lat/lon in 60000ths of a degree
|
|
195
|
+
const lat = rot ? attrInt(rot, 'lat', 0) / 60000 : 0;
|
|
196
|
+
const lon = rot ? attrInt(rot, 'lon', 0) / 60000 : 0;
|
|
197
|
+
const rev = rot ? attrInt(rot, 'rev', 0) / 60000 : 0;
|
|
198
|
+
|
|
199
|
+
// Convert to radians for gentle perspective skew
|
|
200
|
+
const latR = (lat * Math.PI) / 180;
|
|
201
|
+
const lonR = (lon * Math.PI) / 180;
|
|
202
|
+
|
|
203
|
+
// Max skew ±15% of dimension to keep it legible
|
|
204
|
+
const skewX = Math.sin(lonR) * 0.15;
|
|
205
|
+
const skewY = Math.sin(latR) * 0.08;
|
|
206
|
+
|
|
207
|
+
ctx.save();
|
|
208
|
+
ctx.transform(
|
|
209
|
+
1, skewY,
|
|
210
|
+
skewX, 1,
|
|
211
|
+
x * -skewX, y * -skewY // pivot at shape origin
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return () => ctx.restore();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Lighting overlay ──────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Apply a lighting/specular overlay on top of a filled shape.
|
|
221
|
+
*
|
|
222
|
+
* scene3d > lightRig: reads dir, rig
|
|
223
|
+
* Approximated as a directional radial gradient.
|
|
224
|
+
*/
|
|
225
|
+
function drawLighting(ctx, scene3d, x, y, w, h) {
|
|
226
|
+
if (!scene3d) return;
|
|
227
|
+
|
|
228
|
+
const lightRig = g1(scene3d, 'lightRig');
|
|
229
|
+
if (!lightRig) return;
|
|
230
|
+
|
|
231
|
+
const dir = attr(lightRig, 'dir', 't');
|
|
232
|
+
const rig = attr(lightRig, 'rig', 'balanced');
|
|
233
|
+
|
|
234
|
+
// Map light direction to gradient origin
|
|
235
|
+
const dirMap = {
|
|
236
|
+
t: { gx: x + w / 2, gy: y },
|
|
237
|
+
b: { gx: x + w / 2, gy: y + h },
|
|
238
|
+
l: { gx: x, gy: y + h / 2 },
|
|
239
|
+
r: { gx: x + w, gy: y + h / 2 },
|
|
240
|
+
tl: { gx: x, gy: y },
|
|
241
|
+
tr: { gx: x + w, gy: y },
|
|
242
|
+
bl: { gx: x, gy: y + h },
|
|
243
|
+
br: { gx: x + w, gy: y + h },
|
|
244
|
+
};
|
|
245
|
+
const { gx, gy } = dirMap[dir] || dirMap.t;
|
|
246
|
+
|
|
247
|
+
const intensity = rig === 'flat' ? 0.08
|
|
248
|
+
: rig === 'balanced' ? 0.14
|
|
249
|
+
: rig === 'sunrise' ? 0.20
|
|
250
|
+
: rig === 'harsh' ? 0.28
|
|
251
|
+
: 0.12;
|
|
252
|
+
|
|
253
|
+
const radius = Math.sqrt(w * w + h * h);
|
|
254
|
+
const grad = ctx.createRadialGradient(gx, gy, 0, gx, gy, radius);
|
|
255
|
+
grad.addColorStop(0, `rgba(255,255,255,${intensity})`);
|
|
256
|
+
grad.addColorStop(0.5, 'rgba(255,255,255,0)');
|
|
257
|
+
grad.addColorStop(1, `rgba(0,0,0,${intensity * 0.5})`);
|
|
258
|
+
|
|
259
|
+
ctx.save();
|
|
260
|
+
ctx.fillStyle = grad;
|
|
261
|
+
ctx.fillRect(x, y, w, h);
|
|
262
|
+
ctx.restore();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── Main API ─────────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Apply 3D extrusion *before* drawing the shape (so it appears behind).
|
|
269
|
+
*
|
|
270
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
271
|
+
* @param {Element} spPr — shape properties element (or null)
|
|
272
|
+
* @param {object} themeColors
|
|
273
|
+
* @param {number} x, y, w, h — shape bounding box in canvas pixels
|
|
274
|
+
* @param {number} scale
|
|
275
|
+
* @returns {{ applyCamera: function, overlay: function }} — call overlay() after filling the shape
|
|
276
|
+
*/
|
|
277
|
+
export function setup3D(ctx, spPr, themeColors, x, y, w, h, scale) {
|
|
278
|
+
if (!spPr) return { applyCamera: () => () => {}, overlay: () => {} };
|
|
279
|
+
|
|
280
|
+
const sp3d = g1(spPr, 'sp3d') || g1(spPr, 'scene3d')?.parentNode && null;
|
|
281
|
+
const scene3d = g1(spPr, 'scene3d');
|
|
282
|
+
|
|
283
|
+
// Draw extrusion first (behind the shape)
|
|
284
|
+
if (sp3d) {
|
|
285
|
+
drawExtrusion(ctx, sp3d, themeColors, x, y, w, h, scale);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const cleanupCamera = applyCamera(ctx, scene3d, x, y, w, h);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
/** Call this *after* filling the shape to draw bevel + lighting on top. */
|
|
292
|
+
overlay() {
|
|
293
|
+
if (sp3d) {
|
|
294
|
+
drawBevel(ctx, sp3d, x, y, w, h, scale);
|
|
295
|
+
drawContour(ctx, sp3d, themeColors, x, y, w, h, scale);
|
|
296
|
+
}
|
|
297
|
+
if (scene3d) {
|
|
298
|
+
drawLighting(ctx, scene3d, x, y, w, h);
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
/** Call this when done to restore canvas state. */
|
|
302
|
+
cleanup: cleanupCamera,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Simpler function: returns true if spPr contains any 3D directives.
|
|
308
|
+
*/
|
|
309
|
+
export function has3D(spPr) {
|
|
310
|
+
if (!spPr) return false;
|
|
311
|
+
return !!(g1(spPr, 'sp3d') || g1(spPr, 'scene3d'));
|
|
312
|
+
}
|