pptx-angular-viewer 1.1.19 → 1.1.21
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.
|
@@ -1922,9 +1922,9 @@ function getComputedImageStyle(element) {
|
|
|
1922
1922
|
};
|
|
1923
1923
|
}
|
|
1924
1924
|
|
|
1925
|
-
const NONE_PRESETS = new Set(['textNoShape', 'textPlain']);
|
|
1925
|
+
const NONE_PRESETS$1 = new Set(['textNoShape', 'textPlain']);
|
|
1926
1926
|
/** Presets that render best with SVG textPath along curved/circular paths. */
|
|
1927
|
-
const PATH_PRESETS = new Set([
|
|
1927
|
+
const PATH_PRESETS$1 = new Set([
|
|
1928
1928
|
'textArchUp',
|
|
1929
1929
|
'textArchDown',
|
|
1930
1930
|
'textCircle',
|
|
@@ -1948,7 +1948,7 @@ const PATH_PRESETS = new Set([
|
|
|
1948
1948
|
'textStop',
|
|
1949
1949
|
]);
|
|
1950
1950
|
/** Presets that stretch text non-uniformly per line (inflate/deflate/can). */
|
|
1951
|
-
const ENVELOPE_PRESETS = new Set([
|
|
1951
|
+
const ENVELOPE_PRESETS$1 = new Set([
|
|
1952
1952
|
'textInflate',
|
|
1953
1953
|
'textDeflate',
|
|
1954
1954
|
'textInflateBottom',
|
|
@@ -1961,7 +1961,7 @@ const ENVELOPE_PRESETS = new Set([
|
|
|
1961
1961
|
'textCanDown',
|
|
1962
1962
|
]);
|
|
1963
1963
|
/** Presets approximated by basic 2D transforms (slant, fade, cascade). */
|
|
1964
|
-
const SIMPLE_PRESETS = new Set([
|
|
1964
|
+
const SIMPLE_PRESETS$1 = new Set([
|
|
1965
1965
|
'textSlantUp',
|
|
1966
1966
|
'textSlantDown',
|
|
1967
1967
|
'textFadeRight',
|
|
@@ -1978,29 +1978,29 @@ const SIMPLE_PRESETS = new Set([
|
|
|
1978
1978
|
* without an explicit allowlist check.
|
|
1979
1979
|
*/
|
|
1980
1980
|
function classifyTextWarp(preset) {
|
|
1981
|
-
if (!preset || NONE_PRESETS.has(preset)) {
|
|
1981
|
+
if (!preset || NONE_PRESETS$1.has(preset)) {
|
|
1982
1982
|
return 'none';
|
|
1983
1983
|
}
|
|
1984
|
-
if (PATH_PRESETS.has(preset)) {
|
|
1984
|
+
if (PATH_PRESETS$1.has(preset)) {
|
|
1985
1985
|
return 'path';
|
|
1986
1986
|
}
|
|
1987
|
-
if (ENVELOPE_PRESETS.has(preset)) {
|
|
1987
|
+
if (ENVELOPE_PRESETS$1.has(preset)) {
|
|
1988
1988
|
return 'envelope';
|
|
1989
1989
|
}
|
|
1990
|
-
if (SIMPLE_PRESETS.has(preset)) {
|
|
1990
|
+
if (SIMPLE_PRESETS$1.has(preset)) {
|
|
1991
1991
|
return 'simple';
|
|
1992
1992
|
}
|
|
1993
1993
|
return 'none';
|
|
1994
1994
|
}
|
|
1995
1995
|
/** All classified warp presets (excludes `none`). */
|
|
1996
|
-
const ALL_CLASSIFIED_PRESETS = new Set([
|
|
1997
|
-
...NONE_PRESETS,
|
|
1998
|
-
...PATH_PRESETS,
|
|
1999
|
-
...ENVELOPE_PRESETS,
|
|
2000
|
-
...SIMPLE_PRESETS,
|
|
1996
|
+
const ALL_CLASSIFIED_PRESETS$1 = new Set([
|
|
1997
|
+
...NONE_PRESETS$1,
|
|
1998
|
+
...PATH_PRESETS$1,
|
|
1999
|
+
...ENVELOPE_PRESETS$1,
|
|
2000
|
+
...SIMPLE_PRESETS$1,
|
|
2001
2001
|
]);
|
|
2002
2002
|
/** Presets that require SVG textPath rendering (all others fall back to flat). */
|
|
2003
|
-
const SVG_WARP_PRESETS = new Set([
|
|
2003
|
+
const SVG_WARP_PRESETS$1 = new Set([
|
|
2004
2004
|
// Priority 1
|
|
2005
2005
|
'textArchUp',
|
|
2006
2006
|
'textArchDown',
|
|
@@ -2048,7 +2048,7 @@ const SVG_WARP_PRESETS = new Set([
|
|
|
2048
2048
|
const clamp4 = (n) => Math.max(0, Math.min(n, 4));
|
|
2049
2049
|
// ── Priority 1 ──────────────────────────────────────────────────────────
|
|
2050
2050
|
/** Concentric upward arcs. t=0 is the tallest arch. */
|
|
2051
|
-
function archUpPath(w, h, t, adj) {
|
|
2051
|
+
function archUpPath$1(w, h, t, adj) {
|
|
2052
2052
|
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
2053
2053
|
const maxArch = (0.85 * adjNorm) / 0.5;
|
|
2054
2054
|
const archH = h * Math.max(0, maxArch - t * 0.7);
|
|
@@ -2058,7 +2058,7 @@ function archUpPath(w, h, t, adj) {
|
|
|
2058
2058
|
return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
|
|
2059
2059
|
}
|
|
2060
2060
|
/** Concentric downward arcs. t=1 is the deepest. */
|
|
2061
|
-
function archDownPath(w, h, t, adj) {
|
|
2061
|
+
function archDownPath$1(w, h, t, adj) {
|
|
2062
2062
|
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
2063
2063
|
const baseDepth = (0.15 * adjNorm) / 0.5;
|
|
2064
2064
|
const archH = h * (baseDepth + t * 0.7);
|
|
@@ -2068,7 +2068,7 @@ function archDownPath(w, h, t, adj) {
|
|
|
2068
2068
|
return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
|
|
2069
2069
|
}
|
|
2070
2070
|
/** Full ellipse — concentric ellipses shrink towards centre. */
|
|
2071
|
-
function circlePath(w, h, t, adj) {
|
|
2071
|
+
function circlePath$1(w, h, t, adj) {
|
|
2072
2072
|
const cx = w / 2;
|
|
2073
2073
|
const cy = h / 2;
|
|
2074
2074
|
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
@@ -2081,7 +2081,7 @@ function circlePath(w, h, t, adj) {
|
|
|
2081
2081
|
`A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
|
|
2082
2082
|
}
|
|
2083
2083
|
/** Single sine-wave via cubic Bézier. */
|
|
2084
|
-
function wave1Path(w, h, t, adj, adj2) {
|
|
2084
|
+
function wave1Path$1(w, h, t, adj, adj2) {
|
|
2085
2085
|
const yMid = h * (0.25 + t * 0.5);
|
|
2086
2086
|
const adjFactor = adj !== undefined ? adj / 12500 : 1;
|
|
2087
2087
|
const amp = h * 0.2 * clamp4(adjFactor);
|
|
@@ -2091,28 +2091,28 @@ function wave1Path(w, h, t, adj, adj2) {
|
|
|
2091
2091
|
return `M 0,${yMid} C ${cp1x},${yMid - amp} ${cp2x},${yMid + amp} ${w},${yMid}`;
|
|
2092
2092
|
}
|
|
2093
2093
|
/** Inflate — top lines bow up, bottom lines bow down. */
|
|
2094
|
-
function inflatePath(w, h, t, adj) {
|
|
2094
|
+
function inflatePath$1(w, h, t, adj) {
|
|
2095
2095
|
const yBase = h * (0.15 + t * 0.7);
|
|
2096
2096
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2097
2097
|
const bulge = h * 0.3 * (1 - 2 * t) * clamp4(adjFactor);
|
|
2098
2098
|
return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
|
|
2099
2099
|
}
|
|
2100
2100
|
/** Deflate — opposite of inflate. */
|
|
2101
|
-
function deflatePath(w, h, t, adj) {
|
|
2101
|
+
function deflatePath$1(w, h, t, adj) {
|
|
2102
2102
|
const yBase = h * (0.15 + t * 0.7);
|
|
2103
2103
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2104
2104
|
const pinch = h * 0.3 * (2 * t - 1) * clamp4(adjFactor);
|
|
2105
2105
|
return `M 0,${yBase} Q ${w / 2},${yBase - pinch} ${w},${yBase}`;
|
|
2106
2106
|
}
|
|
2107
2107
|
/** Gentle upward curve. */
|
|
2108
|
-
function curveUpPath(w, h, t, adj) {
|
|
2108
|
+
function curveUpPath$1(w, h, t, adj) {
|
|
2109
2109
|
const yBase = h * (0.35 + t * 0.55);
|
|
2110
2110
|
const adjFactor = adj !== undefined ? adj / 45977 : 1;
|
|
2111
2111
|
const curve = h * 0.4 * (1 - t * 0.3) * clamp4(adjFactor);
|
|
2112
2112
|
return `M 0,${yBase} Q ${w / 2},${yBase - curve} ${w},${yBase}`;
|
|
2113
2113
|
}
|
|
2114
2114
|
/** Gentle downward curve. */
|
|
2115
|
-
function curveDownPath(w, h, t, adj) {
|
|
2115
|
+
function curveDownPath$1(w, h, t, adj) {
|
|
2116
2116
|
const yBase = h * (0.1 + t * 0.55);
|
|
2117
2117
|
const adjFactor = adj !== undefined ? adj / 45977 : 1;
|
|
2118
2118
|
const curve = h * 0.4 * (1 - (1 - t) * 0.3) * clamp4(adjFactor);
|
|
@@ -2120,7 +2120,7 @@ function curveDownPath(w, h, t, adj) {
|
|
|
2120
2120
|
}
|
|
2121
2121
|
// ── Priority 2 ──────────────────────────────────────────────────────────
|
|
2122
2122
|
/** Inverted single wave (phase-shifted wave1). */
|
|
2123
|
-
function wave2Path(w, h, t, adj, adj2) {
|
|
2123
|
+
function wave2Path$1(w, h, t, adj, adj2) {
|
|
2124
2124
|
const yMid = h * (0.25 + t * 0.5);
|
|
2125
2125
|
const adjFactor = adj !== undefined ? adj / 12500 : 1;
|
|
2126
2126
|
const amp = h * 0.2 * clamp4(adjFactor);
|
|
@@ -2130,7 +2130,7 @@ function wave2Path(w, h, t, adj, adj2) {
|
|
|
2130
2130
|
return `M 0,${yMid} C ${cp1x},${yMid + amp} ${cp2x},${yMid - amp} ${w},${yMid}`;
|
|
2131
2131
|
}
|
|
2132
2132
|
/** Double wave — two full cycles. */
|
|
2133
|
-
function wave4Path(w, h, t, adj, adj2) {
|
|
2133
|
+
function wave4Path$1(w, h, t, adj, adj2) {
|
|
2134
2134
|
const yMid = h * (0.25 + t * 0.5);
|
|
2135
2135
|
const adjFactor = adj !== undefined ? adj / 12500 : 1;
|
|
2136
2136
|
const amp = h * 0.15 * clamp4(adjFactor);
|
|
@@ -2141,7 +2141,7 @@ function wave4Path(w, h, t, adj, adj2) {
|
|
|
2141
2141
|
`C ${w / 2 + q + hShift},${yMid - amp} ${w - q + hShift},${yMid + amp} ${w},${yMid}`);
|
|
2142
2142
|
}
|
|
2143
2143
|
/** Double wave with alternating rhythm. */
|
|
2144
|
-
function doubleWave1Path(w, h, t, adj, adj2) {
|
|
2144
|
+
function doubleWave1Path$1(w, h, t, adj, adj2) {
|
|
2145
2145
|
const yMid = h * (0.25 + t * 0.5);
|
|
2146
2146
|
const adjFactor = adj !== undefined ? adj / 6250 : 1;
|
|
2147
2147
|
const amp = h * 0.18 * clamp4(adjFactor);
|
|
@@ -2152,7 +2152,7 @@ function doubleWave1Path(w, h, t, adj, adj2) {
|
|
|
2152
2152
|
`C ${w / 2 + q + hShift},${yMid + amp} ${w - q + hShift},${yMid - amp} ${w},${yMid}`);
|
|
2153
2153
|
}
|
|
2154
2154
|
/** Cylindrical text — upward. */
|
|
2155
|
-
function canUpPath(w, h, t, adj) {
|
|
2155
|
+
function canUpPath$1(w, h, t, adj) {
|
|
2156
2156
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2157
2157
|
const curvature = clamp4(adjFactor);
|
|
2158
2158
|
const archH = h * (0.35 - t * 0.25) * curvature;
|
|
@@ -2162,7 +2162,7 @@ function canUpPath(w, h, t, adj) {
|
|
|
2162
2162
|
return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
|
|
2163
2163
|
}
|
|
2164
2164
|
/** Cylindrical text — downward. */
|
|
2165
|
-
function canDownPath(w, h, t, adj) {
|
|
2165
|
+
function canDownPath$1(w, h, t, adj) {
|
|
2166
2166
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2167
2167
|
const curvature = clamp4(adjFactor);
|
|
2168
2168
|
const archH = h * (0.1 + t * 0.25) * curvature;
|
|
@@ -2172,14 +2172,14 @@ function canDownPath(w, h, t, adj) {
|
|
|
2172
2172
|
return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
|
|
2173
2173
|
}
|
|
2174
2174
|
/** Button — convex top / concave bottom. */
|
|
2175
|
-
function buttonPath(w, h, t, adj) {
|
|
2175
|
+
function buttonPath$1(w, h, t, adj) {
|
|
2176
2176
|
const yBase = h * (0.1 + t * 0.8);
|
|
2177
2177
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2178
2178
|
const bulge = h * 0.15 * (1 - 2 * t) * clamp4(adjFactor);
|
|
2179
2179
|
return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
|
|
2180
2180
|
}
|
|
2181
2181
|
/** Ring inside — concentric ellipses scaled inward. */
|
|
2182
|
-
function ringInsidePath(w, h, t, adj) {
|
|
2182
|
+
function ringInsidePath$1(w, h, t, adj) {
|
|
2183
2183
|
const cx = w / 2;
|
|
2184
2184
|
const cy = h / 2;
|
|
2185
2185
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
@@ -2192,7 +2192,7 @@ function ringInsidePath(w, h, t, adj) {
|
|
|
2192
2192
|
`A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
|
|
2193
2193
|
}
|
|
2194
2194
|
/** Ring outside — concentric ellipses scaled outward. */
|
|
2195
|
-
function ringOutsidePath(w, h, t, adj) {
|
|
2195
|
+
function ringOutsidePath$1(w, h, t, adj) {
|
|
2196
2196
|
const cx = w / 2;
|
|
2197
2197
|
const cy = h / 2;
|
|
2198
2198
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
@@ -2205,7 +2205,7 @@ function ringOutsidePath(w, h, t, adj) {
|
|
|
2205
2205
|
`A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
|
|
2206
2206
|
}
|
|
2207
2207
|
/** Cascading up — lines tilt from lower-left to upper-right. */
|
|
2208
|
-
function cascadeUpPath(w, h, t, adj) {
|
|
2208
|
+
function cascadeUpPath$1(w, h, t, adj) {
|
|
2209
2209
|
const adjFactor = adj !== undefined ? adj / 44444 : 1;
|
|
2210
2210
|
const tilt = 0.2 * clamp4(adjFactor);
|
|
2211
2211
|
const yMid = h * (0.2 + t * 0.6);
|
|
@@ -2214,7 +2214,7 @@ function cascadeUpPath(w, h, t, adj) {
|
|
|
2214
2214
|
return `M 0,${yStart} L ${w},${yEnd}`;
|
|
2215
2215
|
}
|
|
2216
2216
|
/** Cascading down — lines tilt from upper-left to lower-right. */
|
|
2217
|
-
function cascadeDownPath(w, h, t, adj) {
|
|
2217
|
+
function cascadeDownPath$1(w, h, t, adj) {
|
|
2218
2218
|
const adjFactor = adj !== undefined ? adj / 44444 : 1;
|
|
2219
2219
|
const tilt = 0.2 * clamp4(adjFactor);
|
|
2220
2220
|
const yMid = h * (0.2 + t * 0.6);
|
|
@@ -2224,7 +2224,7 @@ function cascadeDownPath(w, h, t, adj) {
|
|
|
2224
2224
|
}
|
|
2225
2225
|
// ── Priority 3 ──────────────────────────────────────────────────────────
|
|
2226
2226
|
/** Triangle / trapezoid — top line narrow, bottom line full width. */
|
|
2227
|
-
function trianglePath(w, h, t, adj) {
|
|
2227
|
+
function trianglePath$1(w, h, t, adj) {
|
|
2228
2228
|
const adjRatio = adj !== undefined ? adj / 100000 : 0.5;
|
|
2229
2229
|
const narrowW = w * (1 - Math.max(0, Math.min(adjRatio, 1))) * 0.3;
|
|
2230
2230
|
const lineW = narrowW + t * (w - narrowW);
|
|
@@ -2233,7 +2233,7 @@ function trianglePath(w, h, t, adj) {
|
|
|
2233
2233
|
return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
|
|
2234
2234
|
}
|
|
2235
2235
|
/** Inverted triangle — top line full width, bottom line narrow. */
|
|
2236
|
-
function triangleInvertedPath(w, h, t, adj) {
|
|
2236
|
+
function triangleInvertedPath$1(w, h, t, adj) {
|
|
2237
2237
|
const adjRatio = adj !== undefined ? adj / 100000 : 0.5;
|
|
2238
2238
|
const narrowW = w * (1 - Math.max(0, Math.min(adjRatio, 1))) * 0.3;
|
|
2239
2239
|
const lineW = w - t * (w - narrowW);
|
|
@@ -2242,7 +2242,7 @@ function triangleInvertedPath(w, h, t, adj) {
|
|
|
2242
2242
|
return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
|
|
2243
2243
|
}
|
|
2244
2244
|
/** Stop / octagon — lines narrow at top and bottom, widest in centre. */
|
|
2245
|
-
function stopPath(w, h, t, adj) {
|
|
2245
|
+
function stopPath$1(w, h, t, adj) {
|
|
2246
2246
|
const adjFactor = adj !== undefined ? adj / 25000 : 1;
|
|
2247
2247
|
const insetScale = clamp4(adjFactor);
|
|
2248
2248
|
const inset = w * 0.15 * (1 - (1 - 2 * Math.abs(t - 0.5)) ** 2) * insetScale;
|
|
@@ -2250,42 +2250,42 @@ function stopPath(w, h, t, adj) {
|
|
|
2250
2250
|
return `M ${inset},${yBase} L ${w - inset},${yBase}`;
|
|
2251
2251
|
}
|
|
2252
2252
|
/** Chevron — V-shape pointing down. */
|
|
2253
|
-
function chevronPath(w, h, t, adj) {
|
|
2253
|
+
function chevronPath$1(w, h, t, adj) {
|
|
2254
2254
|
const yBase = h * (0.15 + t * 0.7);
|
|
2255
2255
|
const adjFactor = adj !== undefined ? adj / 25000 : 1;
|
|
2256
2256
|
const dip = h * 0.2 * (1 - t) * clamp4(adjFactor);
|
|
2257
2257
|
return `M 0,${yBase} L ${w / 2},${yBase + dip} L ${w},${yBase}`;
|
|
2258
2258
|
}
|
|
2259
2259
|
/** Inverted chevron — V-shape pointing up. */
|
|
2260
|
-
function chevronInvertedPath(w, h, t, adj) {
|
|
2260
|
+
function chevronInvertedPath$1(w, h, t, adj) {
|
|
2261
2261
|
const yBase = h * (0.15 + t * 0.7);
|
|
2262
2262
|
const adjFactor = adj !== undefined ? adj / 25000 : 1;
|
|
2263
2263
|
const rise = h * 0.2 * t * clamp4(adjFactor);
|
|
2264
2264
|
return `M 0,${yBase} L ${w / 2},${yBase - rise} L ${w},${yBase}`;
|
|
2265
2265
|
}
|
|
2266
2266
|
/** Inflate bottom only. */
|
|
2267
|
-
function inflateBottomPath(w, h, t, adj) {
|
|
2267
|
+
function inflateBottomPath$1(w, h, t, adj) {
|
|
2268
2268
|
const yBase = h * (0.15 + t * 0.7);
|
|
2269
2269
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2270
2270
|
const bulge = t > 0.4 ? h * 0.25 * ((t - 0.4) / 0.6) * clamp4(adjFactor) : 0;
|
|
2271
2271
|
return `M 0,${yBase} Q ${w / 2},${yBase + bulge} ${w},${yBase}`;
|
|
2272
2272
|
}
|
|
2273
2273
|
/** Inflate top only. */
|
|
2274
|
-
function inflateTopPath(w, h, t, adj) {
|
|
2274
|
+
function inflateTopPath$1(w, h, t, adj) {
|
|
2275
2275
|
const yBase = h * (0.15 + t * 0.7);
|
|
2276
2276
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2277
2277
|
const bulge = t < 0.6 ? h * 0.25 * ((0.6 - t) / 0.6) * clamp4(adjFactor) : 0;
|
|
2278
2278
|
return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
|
|
2279
2279
|
}
|
|
2280
2280
|
/** Deflate bottom only. */
|
|
2281
|
-
function deflateBottomPath(w, h, t, adj) {
|
|
2281
|
+
function deflateBottomPath$1(w, h, t, adj) {
|
|
2282
2282
|
const yBase = h * (0.15 + t * 0.7);
|
|
2283
2283
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2284
2284
|
const pinch = t > 0.4 ? h * 0.2 * ((t - 0.4) / 0.6) * clamp4(adjFactor) : 0;
|
|
2285
2285
|
return `M 0,${yBase} Q ${w / 2},${yBase - pinch} ${w},${yBase}`;
|
|
2286
2286
|
}
|
|
2287
2287
|
/** Deflate top only. */
|
|
2288
|
-
function deflateTopPath(w, h, t, adj) {
|
|
2288
|
+
function deflateTopPath$1(w, h, t, adj) {
|
|
2289
2289
|
const yBase = h * (0.15 + t * 0.7);
|
|
2290
2290
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2291
2291
|
const pinch = t < 0.6 ? h * 0.2 * ((0.6 - t) / 0.6) * clamp4(adjFactor) : 0;
|
|
@@ -2293,7 +2293,7 @@ function deflateTopPath(w, h, t, adj) {
|
|
|
2293
2293
|
}
|
|
2294
2294
|
// ── Priority 4 ──────────────────────────────────────────────────────────
|
|
2295
2295
|
/** Slant up — baseline rises from left to right. */
|
|
2296
|
-
function slantUpPath(w, h, t, adj) {
|
|
2296
|
+
function slantUpPath$1(w, h, t, adj) {
|
|
2297
2297
|
const adjFactor = adj !== undefined ? adj / 55000 : 1;
|
|
2298
2298
|
const slant = 0.25 * clamp4(adjFactor);
|
|
2299
2299
|
const yMid = h * (0.175 + t * 0.55);
|
|
@@ -2302,7 +2302,7 @@ function slantUpPath(w, h, t, adj) {
|
|
|
2302
2302
|
return `M 0,${yStart} L ${w},${yEnd}`;
|
|
2303
2303
|
}
|
|
2304
2304
|
/** Slant down — baseline falls from left to right. */
|
|
2305
|
-
function slantDownPath(w, h, t, adj) {
|
|
2305
|
+
function slantDownPath$1(w, h, t, adj) {
|
|
2306
2306
|
const adjFactor = adj !== undefined ? adj / 55000 : 1;
|
|
2307
2307
|
const slant = 0.25 * clamp4(adjFactor);
|
|
2308
2308
|
const yMid = h * (0.175 + t * 0.55);
|
|
@@ -2311,7 +2311,7 @@ function slantDownPath(w, h, t, adj) {
|
|
|
2311
2311
|
return `M 0,${yStart} L ${w},${yEnd}`;
|
|
2312
2312
|
}
|
|
2313
2313
|
/** Fade right — text narrows towards the right (trapezoid). */
|
|
2314
|
-
function fadeRightPath(w, h, t, adj) {
|
|
2314
|
+
function fadeRightPath$1(w, h, t, adj) {
|
|
2315
2315
|
const adjFactor = adj !== undefined ? adj / 50000 : 1;
|
|
2316
2316
|
const squeezeScale = clamp4(adjFactor);
|
|
2317
2317
|
const yLeft = h * (0.1 + t * 0.8);
|
|
@@ -2320,7 +2320,7 @@ function fadeRightPath(w, h, t, adj) {
|
|
|
2320
2320
|
return `M 0,${yLeft} L ${w},${yRight}`;
|
|
2321
2321
|
}
|
|
2322
2322
|
/** Fade left — text narrows towards the left (trapezoid). */
|
|
2323
|
-
function fadeLeftPath(w, h, t, adj) {
|
|
2323
|
+
function fadeLeftPath$1(w, h, t, adj) {
|
|
2324
2324
|
const adjFactor = adj !== undefined ? adj / 50000 : 1;
|
|
2325
2325
|
const squeezeScale = clamp4(adjFactor);
|
|
2326
2326
|
const squeeze = 0.35 * (1 - 2 * t) * squeezeScale;
|
|
@@ -2329,7 +2329,7 @@ function fadeLeftPath(w, h, t, adj) {
|
|
|
2329
2329
|
return `M 0,${yLeft} L ${w},${yRight}`;
|
|
2330
2330
|
}
|
|
2331
2331
|
/** Fade up — text narrows towards the top. */
|
|
2332
|
-
function fadeUpPath(w, h, t, adj) {
|
|
2332
|
+
function fadeUpPath$1(w, h, t, adj) {
|
|
2333
2333
|
const adjFactor = adj !== undefined ? adj / 50000 : 1;
|
|
2334
2334
|
const taperScale = clamp4(adjFactor);
|
|
2335
2335
|
const narrowFraction = 1 - 0.7 * taperScale;
|
|
@@ -2340,7 +2340,7 @@ function fadeUpPath(w, h, t, adj) {
|
|
|
2340
2340
|
return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
|
|
2341
2341
|
}
|
|
2342
2342
|
/** Fade down — text narrows towards the bottom. */
|
|
2343
|
-
function fadeDownPath(w, h, t, adj) {
|
|
2343
|
+
function fadeDownPath$1(w, h, t, adj) {
|
|
2344
2344
|
const adjFactor = adj !== undefined ? adj / 50000 : 1;
|
|
2345
2345
|
const taperScale = clamp4(adjFactor);
|
|
2346
2346
|
const narrowFraction = 1 - 0.7 * taperScale;
|
|
@@ -2351,7 +2351,7 @@ function fadeDownPath(w, h, t, adj) {
|
|
|
2351
2351
|
return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
|
|
2352
2352
|
}
|
|
2353
2353
|
/** Arch up pour — hollowed arch upward. */
|
|
2354
|
-
function archUpPourPath(w, h, t, adj) {
|
|
2354
|
+
function archUpPourPath$1(w, h, t, adj) {
|
|
2355
2355
|
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
2356
2356
|
const maxArch = (0.7 * adjNorm) / 0.5;
|
|
2357
2357
|
const archH = h * Math.max(0, maxArch - t * 0.5);
|
|
@@ -2361,7 +2361,7 @@ function archUpPourPath(w, h, t, adj) {
|
|
|
2361
2361
|
return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
|
|
2362
2362
|
}
|
|
2363
2363
|
/** Arch down pour — hollowed arch downward. */
|
|
2364
|
-
function archDownPourPath(w, h, t, adj) {
|
|
2364
|
+
function archDownPourPath$1(w, h, t, adj) {
|
|
2365
2365
|
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
2366
2366
|
const baseDepth = (0.2 * adjNorm) / 0.5;
|
|
2367
2367
|
const archH = h * (baseDepth + t * 0.5);
|
|
@@ -2371,7 +2371,7 @@ function archDownPourPath(w, h, t, adj) {
|
|
|
2371
2371
|
return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
|
|
2372
2372
|
}
|
|
2373
2373
|
/** Circle pour — concentric ellipses with an inner gap. */
|
|
2374
|
-
function circlePourPath(w, h, t, adj) {
|
|
2374
|
+
function circlePourPath$1(w, h, t, adj) {
|
|
2375
2375
|
const cx = w / 2;
|
|
2376
2376
|
const cy = h / 2;
|
|
2377
2377
|
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
@@ -2384,14 +2384,14 @@ function circlePourPath(w, h, t, adj) {
|
|
|
2384
2384
|
`A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
|
|
2385
2385
|
}
|
|
2386
2386
|
/** Button pour — convex top / concave bottom with larger margins. */
|
|
2387
|
-
function buttonPourPath(w, h, t, adj) {
|
|
2387
|
+
function buttonPourPath$1(w, h, t, adj) {
|
|
2388
2388
|
const yBase = h * (0.15 + t * 0.7);
|
|
2389
2389
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2390
2390
|
const bulge = h * 0.12 * (1 - 2 * t) * clamp4(adjFactor);
|
|
2391
2391
|
return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
|
|
2392
2392
|
}
|
|
2393
2393
|
/** Deflate-inflate — pinched in centre top/bottom, expanded at edges. */
|
|
2394
|
-
function deflateInflatePath(w, h, t, adj) {
|
|
2394
|
+
function deflateInflatePath$1(w, h, t, adj) {
|
|
2395
2395
|
const yBase = h * (0.15 + t * 0.7);
|
|
2396
2396
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2397
2397
|
const factor = Math.sin(t * Math.PI);
|
|
@@ -2399,7 +2399,7 @@ function deflateInflatePath(w, h, t, adj) {
|
|
|
2399
2399
|
return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
|
|
2400
2400
|
}
|
|
2401
2401
|
/** Deflate-inflate-deflate — triple oscillation. */
|
|
2402
|
-
function deflateInflateDeflatePath(w, h, t, adj) {
|
|
2402
|
+
function deflateInflateDeflatePath$1(w, h, t, adj) {
|
|
2403
2403
|
const yBase = h * (0.15 + t * 0.7);
|
|
2404
2404
|
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
2405
2405
|
const ampScale = clamp4(adjFactor);
|
|
@@ -2412,54 +2412,54 @@ function deflateInflateDeflatePath(w, h, t, adj) {
|
|
|
2412
2412
|
`Q ${q2},${yBase + bulge} ${w},${yBase}`);
|
|
2413
2413
|
}
|
|
2414
2414
|
// ── Generator look-up table ─────────────────────────────────────────────
|
|
2415
|
-
const WARP_PATH_GENERATORS = {
|
|
2416
|
-
textArchUp: archUpPath,
|
|
2417
|
-
textArchDown: archDownPath,
|
|
2418
|
-
textCircle: circlePath,
|
|
2419
|
-
textWave1: wave1Path,
|
|
2420
|
-
textInflate: inflatePath,
|
|
2421
|
-
textDeflate: deflatePath,
|
|
2422
|
-
textCurveUp: curveUpPath,
|
|
2423
|
-
textCurveDown: curveDownPath,
|
|
2424
|
-
textWave2: wave2Path,
|
|
2425
|
-
textWave4: wave4Path,
|
|
2426
|
-
textDoubleWave1: doubleWave1Path,
|
|
2427
|
-
textCanUp: canUpPath,
|
|
2428
|
-
textCanDown: canDownPath,
|
|
2429
|
-
textButton: buttonPath,
|
|
2430
|
-
textRingInside: ringInsidePath,
|
|
2431
|
-
textRingOutside: ringOutsidePath,
|
|
2432
|
-
textCascadeUp: cascadeUpPath,
|
|
2433
|
-
textCascadeDown: cascadeDownPath,
|
|
2434
|
-
textTriangle: trianglePath,
|
|
2435
|
-
textTriangleInverted: triangleInvertedPath,
|
|
2436
|
-
textStop: stopPath,
|
|
2437
|
-
textChevron: chevronPath,
|
|
2438
|
-
textChevronInverted: chevronInvertedPath,
|
|
2439
|
-
textInflateBottom: inflateBottomPath,
|
|
2440
|
-
textInflateTop: inflateTopPath,
|
|
2441
|
-
textDeflateBottom: deflateBottomPath,
|
|
2442
|
-
textDeflateTop: deflateTopPath,
|
|
2443
|
-
textSlantUp: slantUpPath,
|
|
2444
|
-
textSlantDown: slantDownPath,
|
|
2445
|
-
textFadeRight: fadeRightPath,
|
|
2446
|
-
textFadeLeft: fadeLeftPath,
|
|
2447
|
-
textFadeUp: fadeUpPath,
|
|
2448
|
-
textFadeDown: fadeDownPath,
|
|
2449
|
-
textArchUpPour: archUpPourPath,
|
|
2450
|
-
textArchDownPour: archDownPourPath,
|
|
2451
|
-
textCirclePour: circlePourPath,
|
|
2452
|
-
textButtonPour: buttonPourPath,
|
|
2453
|
-
textDeflateInflate: deflateInflatePath,
|
|
2454
|
-
textDeflateInflateDeflate: deflateInflateDeflatePath,
|
|
2415
|
+
const WARP_PATH_GENERATORS$1 = {
|
|
2416
|
+
textArchUp: archUpPath$1,
|
|
2417
|
+
textArchDown: archDownPath$1,
|
|
2418
|
+
textCircle: circlePath$1,
|
|
2419
|
+
textWave1: wave1Path$1,
|
|
2420
|
+
textInflate: inflatePath$1,
|
|
2421
|
+
textDeflate: deflatePath$1,
|
|
2422
|
+
textCurveUp: curveUpPath$1,
|
|
2423
|
+
textCurveDown: curveDownPath$1,
|
|
2424
|
+
textWave2: wave2Path$1,
|
|
2425
|
+
textWave4: wave4Path$1,
|
|
2426
|
+
textDoubleWave1: doubleWave1Path$1,
|
|
2427
|
+
textCanUp: canUpPath$1,
|
|
2428
|
+
textCanDown: canDownPath$1,
|
|
2429
|
+
textButton: buttonPath$1,
|
|
2430
|
+
textRingInside: ringInsidePath$1,
|
|
2431
|
+
textRingOutside: ringOutsidePath$1,
|
|
2432
|
+
textCascadeUp: cascadeUpPath$1,
|
|
2433
|
+
textCascadeDown: cascadeDownPath$1,
|
|
2434
|
+
textTriangle: trianglePath$1,
|
|
2435
|
+
textTriangleInverted: triangleInvertedPath$1,
|
|
2436
|
+
textStop: stopPath$1,
|
|
2437
|
+
textChevron: chevronPath$1,
|
|
2438
|
+
textChevronInverted: chevronInvertedPath$1,
|
|
2439
|
+
textInflateBottom: inflateBottomPath$1,
|
|
2440
|
+
textInflateTop: inflateTopPath$1,
|
|
2441
|
+
textDeflateBottom: deflateBottomPath$1,
|
|
2442
|
+
textDeflateTop: deflateTopPath$1,
|
|
2443
|
+
textSlantUp: slantUpPath$1,
|
|
2444
|
+
textSlantDown: slantDownPath$1,
|
|
2445
|
+
textFadeRight: fadeRightPath$1,
|
|
2446
|
+
textFadeLeft: fadeLeftPath$1,
|
|
2447
|
+
textFadeUp: fadeUpPath$1,
|
|
2448
|
+
textFadeDown: fadeDownPath$1,
|
|
2449
|
+
textArchUpPour: archUpPourPath$1,
|
|
2450
|
+
textArchDownPour: archDownPourPath$1,
|
|
2451
|
+
textCirclePour: circlePourPath$1,
|
|
2452
|
+
textButtonPour: buttonPourPath$1,
|
|
2453
|
+
textDeflateInflate: deflateInflatePath$1,
|
|
2454
|
+
textDeflateInflateDeflate: deflateInflateDeflatePath$1,
|
|
2455
2455
|
};
|
|
2456
2456
|
// ── Public API ──────────────────────────────────────────────────────────
|
|
2457
2457
|
/** Returns `true` when the preset should use SVG `<textPath>` rendering. */
|
|
2458
|
-
function shouldUseSvgWarp(preset) {
|
|
2458
|
+
function shouldUseSvgWarp$1(preset) {
|
|
2459
2459
|
if (!preset || preset === 'textNoShape' || preset === 'textPlain') {
|
|
2460
2460
|
return false;
|
|
2461
2461
|
}
|
|
2462
|
-
return SVG_WARP_PRESETS.has(preset);
|
|
2462
|
+
return SVG_WARP_PRESETS$1.has(preset);
|
|
2463
2463
|
}
|
|
2464
2464
|
/**
|
|
2465
2465
|
* Predicate: does this element carry a WordArt/text-warp preset that the SVG
|
|
@@ -2470,7 +2470,7 @@ function hasTextWarp(element) {
|
|
|
2470
2470
|
if (!hasTextProperties(element)) {
|
|
2471
2471
|
return false;
|
|
2472
2472
|
}
|
|
2473
|
-
return shouldUseSvgWarp(element.textStyle?.textWarpPreset);
|
|
2473
|
+
return shouldUseSvgWarp$1(element.textStyle?.textWarpPreset);
|
|
2474
2474
|
}
|
|
2475
2475
|
/**
|
|
2476
2476
|
* Build the SVG path `d` attribute for a warp preset at a given line position.
|
|
@@ -2485,7 +2485,7 @@ function hasTextWarp(element) {
|
|
|
2485
2485
|
*/
|
|
2486
2486
|
function buildWarpPath(preset, width, height, lineIndex, lineCount, adj, adj2) {
|
|
2487
2487
|
const t = lineCount <= 1 ? 0.5 : lineIndex / (lineCount - 1);
|
|
2488
|
-
const generator = WARP_PATH_GENERATORS[preset];
|
|
2488
|
+
const generator = WARP_PATH_GENERATORS$1[preset];
|
|
2489
2489
|
if (generator) {
|
|
2490
2490
|
return generator(width, height, t, adj, adj2);
|
|
2491
2491
|
}
|
|
@@ -4023,7 +4023,7 @@ function resolveCategoryLabels(chartData) {
|
|
|
4023
4023
|
* `mode === 'bar'` centres on category slots; `'line'` anchors at points.
|
|
4024
4024
|
* Mirrors the React `xToPixel`.
|
|
4025
4025
|
*/
|
|
4026
|
-
function xToPixel(xVal, catCount, layout, mode) {
|
|
4026
|
+
function xToPixel$1(xVal, catCount, layout, mode) {
|
|
4027
4027
|
if (mode === 'bar') {
|
|
4028
4028
|
const slotWidth = layout.plotWidth / Math.max(catCount, 1);
|
|
4029
4029
|
return layout.plotLeft + slotWidth * xVal + slotWidth / 2;
|
|
@@ -4032,7 +4032,7 @@ function xToPixel(xVal, catCount, layout, mode) {
|
|
|
4032
4032
|
return layout.plotLeft + (xVal / maxIdx) * layout.plotWidth;
|
|
4033
4033
|
}
|
|
4034
4034
|
/** Ordinary least-squares linear regression of `yVals` on `xVals`. */
|
|
4035
|
-
function computeLinearRegression(xVals, yVals) {
|
|
4035
|
+
function computeLinearRegression$1(xVals, yVals) {
|
|
4036
4036
|
const n = xVals.length;
|
|
4037
4037
|
if (n < 2) {
|
|
4038
4038
|
return { slope: 0, intercept: 0, rSquared: 0 };
|
|
@@ -4060,7 +4060,7 @@ function computeLinearRegression(xVals, yVals) {
|
|
|
4060
4060
|
return { slope, intercept, rSquared };
|
|
4061
4061
|
}
|
|
4062
4062
|
/** Fit polynomial coefficients (ascending order) via Gaussian elimination. */
|
|
4063
|
-
function fitPolynomial(xVals, yVals, order) {
|
|
4063
|
+
function fitPolynomial$1(xVals, yVals, order) {
|
|
4064
4064
|
const n = xVals.length;
|
|
4065
4065
|
const m = order + 1;
|
|
4066
4066
|
const matrix = Array.from({ length: m }, () => Array(m + 1).fill(0));
|
|
@@ -4106,7 +4106,7 @@ function fitPolynomial(xVals, yVals, order) {
|
|
|
4106
4106
|
return matrix.map((row) => row[m]);
|
|
4107
4107
|
}
|
|
4108
4108
|
/** R² of an arbitrary fit function against the data. */
|
|
4109
|
-
function computeRSquared(xVals, yVals, evalFn) {
|
|
4109
|
+
function computeRSquared$1(xVals, yVals, evalFn) {
|
|
4110
4110
|
const n = xVals.length;
|
|
4111
4111
|
const meanY = yVals.reduce((s, y) => s + y, 0) / n;
|
|
4112
4112
|
let ssRes = 0;
|
|
@@ -4122,7 +4122,7 @@ function computeRSquared(xVals, yVals, evalFn) {
|
|
|
4122
4122
|
* series' values. Returns an empty point list when the type is unsupported or
|
|
4123
4123
|
* the data is too sparse to fit. Mirrors the React `computeTrendlinePoints`.
|
|
4124
4124
|
*/
|
|
4125
|
-
function computeTrendlinePoints(trendline, values, catCount, layout, range, mode) {
|
|
4125
|
+
function computeTrendlinePoints$1(trendline, values, catCount, layout, range, mode) {
|
|
4126
4126
|
const n = values.length;
|
|
4127
4127
|
if (n < 2) {
|
|
4128
4128
|
return { points: [], equation: '', rSquared: 0 };
|
|
@@ -4139,7 +4139,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
|
|
|
4139
4139
|
let rSquared = 0;
|
|
4140
4140
|
switch (trendline.trendlineType) {
|
|
4141
4141
|
case 'linear': {
|
|
4142
|
-
const reg = computeLinearRegression(xVals, yVals);
|
|
4142
|
+
const reg = computeLinearRegression$1(xVals, yVals);
|
|
4143
4143
|
const intercept = trendline.intercept;
|
|
4144
4144
|
const slope = intercept !== undefined
|
|
4145
4145
|
? yVals.reduce((s, y, i) => s + (y - intercept) * xVals[i], 0) /
|
|
@@ -4157,7 +4157,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
|
|
|
4157
4157
|
if (logY.length < 2) {
|
|
4158
4158
|
return { points: [], equation: '', rSquared: 0 };
|
|
4159
4159
|
}
|
|
4160
|
-
const reg = computeLinearRegression(filteredX, logY);
|
|
4160
|
+
const reg = computeLinearRegression$1(filteredX, logY);
|
|
4161
4161
|
const a = Math.exp(reg.intercept);
|
|
4162
4162
|
const b = reg.slope;
|
|
4163
4163
|
evalFn = (x) => a * Math.exp(b * x);
|
|
@@ -4171,7 +4171,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
|
|
|
4171
4171
|
if (lnX.length < 2) {
|
|
4172
4172
|
return { points: [], equation: '', rSquared: 0 };
|
|
4173
4173
|
}
|
|
4174
|
-
const reg = computeLinearRegression(lnX, filteredY);
|
|
4174
|
+
const reg = computeLinearRegression$1(lnX, filteredY);
|
|
4175
4175
|
evalFn = (x) => (x > 0 ? reg.slope * Math.log(x) + reg.intercept : 0);
|
|
4176
4176
|
equation = `y = ${reg.slope.toFixed(2)}ln(x) + ${reg.intercept.toFixed(2)}`;
|
|
4177
4177
|
rSquared = reg.rSquared;
|
|
@@ -4183,7 +4183,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
|
|
|
4183
4183
|
if (logX.length < 2) {
|
|
4184
4184
|
return { points: [], equation: '', rSquared: 0 };
|
|
4185
4185
|
}
|
|
4186
|
-
const reg = computeLinearRegression(logX, logY);
|
|
4186
|
+
const reg = computeLinearRegression$1(logX, logY);
|
|
4187
4187
|
const a = Math.exp(reg.intercept);
|
|
4188
4188
|
evalFn = (x) => (x > 0 ? a * x ** reg.slope : 0);
|
|
4189
4189
|
equation = `y = ${a.toFixed(2)}x^${reg.slope.toFixed(2)}`;
|
|
@@ -4192,10 +4192,10 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
|
|
|
4192
4192
|
}
|
|
4193
4193
|
case 'polynomial': {
|
|
4194
4194
|
const order = Math.min(trendline.order ?? 2, 6);
|
|
4195
|
-
const coeffs = fitPolynomial(xVals, yVals, order);
|
|
4195
|
+
const coeffs = fitPolynomial$1(xVals, yVals, order);
|
|
4196
4196
|
evalFn = (x) => coeffs.reduce((s, c, i) => s + c * x ** i, 0);
|
|
4197
4197
|
equation = coeffs.map((c, i) => `${c.toFixed(2)}x^${i}`).join(' + ');
|
|
4198
|
-
rSquared = computeRSquared(xVals, yVals, evalFn);
|
|
4198
|
+
rSquared = computeRSquared$1(xVals, yVals, evalFn);
|
|
4199
4199
|
break;
|
|
4200
4200
|
}
|
|
4201
4201
|
case 'movingAvg': {
|
|
@@ -4207,7 +4207,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
|
|
|
4207
4207
|
sum += yVals[j];
|
|
4208
4208
|
}
|
|
4209
4209
|
const avgVal = sum / period;
|
|
4210
|
-
const px = xToPixel(i, catCount, layout, mode);
|
|
4210
|
+
const px = xToPixel$1(i, catCount, layout, mode);
|
|
4211
4211
|
const py = valueToYLocal(avgVal, range, layout.plotTop, layout.plotBottom);
|
|
4212
4212
|
maPoints.push({ x: px, y: py });
|
|
4213
4213
|
}
|
|
@@ -4227,7 +4227,7 @@ function computeTrendlinePoints(trendline, values, catCount, layout, range, mode
|
|
|
4227
4227
|
if (!Number.isFinite(yVal)) {
|
|
4228
4228
|
continue;
|
|
4229
4229
|
}
|
|
4230
|
-
const px = xToPixel(xVal, catCount, layout, mode);
|
|
4230
|
+
const px = xToPixel$1(xVal, catCount, layout, mode);
|
|
4231
4231
|
const py = valueToYLocal(yVal, range, layout.plotTop, layout.plotBottom);
|
|
4232
4232
|
points.push({ x: px, y: py });
|
|
4233
4233
|
}
|
|
@@ -4257,7 +4257,7 @@ function computeChartTrendlines(chartData, layout, range, mode, styleId, colorPa
|
|
|
4257
4257
|
return;
|
|
4258
4258
|
}
|
|
4259
4259
|
series.trendlines.forEach((tl) => {
|
|
4260
|
-
const { points, equation, rSquared } = computeTrendlinePoints(tl, series.values, catCount, layout, range, mode);
|
|
4260
|
+
const { points, equation, rSquared } = computeTrendlinePoints$1(tl, series.values, catCount, layout, range, mode);
|
|
4261
4261
|
if (points.length < 2) {
|
|
4262
4262
|
return;
|
|
4263
4263
|
}
|
|
@@ -16015,7 +16015,7 @@ function stringFromEvent$2(event) {
|
|
|
16015
16015
|
const DEFAULT_FILL = '#ffffff';
|
|
16016
16016
|
const DEFAULT_STROKE = '#000000';
|
|
16017
16017
|
const DEFAULT_TEXT_COLOR = '#000000';
|
|
16018
|
-
const DEFAULT_FONT_SIZE = 12;
|
|
16018
|
+
const DEFAULT_FONT_SIZE$1 = 12;
|
|
16019
16019
|
// ── Readers ──────────────────────────────────────────────────────────────────
|
|
16020
16020
|
/**
|
|
16021
16021
|
* Returns the fill colour of the element's shapeStyle, or a white default.
|
|
@@ -16053,9 +16053,9 @@ function textColorOf(el) {
|
|
|
16053
16053
|
*/
|
|
16054
16054
|
function fontSizeOf(el) {
|
|
16055
16055
|
if (hasTextProperties(el)) {
|
|
16056
|
-
return el.textStyle?.fontSize ?? DEFAULT_FONT_SIZE;
|
|
16056
|
+
return el.textStyle?.fontSize ?? DEFAULT_FONT_SIZE$1;
|
|
16057
16057
|
}
|
|
16058
|
-
return DEFAULT_FONT_SIZE;
|
|
16058
|
+
return DEFAULT_FONT_SIZE$1;
|
|
16059
16059
|
}
|
|
16060
16060
|
/**
|
|
16061
16061
|
* Returns whether the element's text is bold (false when absent).
|
|
@@ -19610,6 +19610,730 @@ function buildStockViewModel(element, chartData, categoryLabels) {
|
|
|
19610
19610
|
};
|
|
19611
19611
|
}
|
|
19612
19612
|
|
|
19613
|
+
/**
|
|
19614
|
+
* chart-overlays.ts — chart overlay depth for Angular pptx-angular-viewer.
|
|
19615
|
+
*
|
|
19616
|
+
* Pure functions that produce additional `SvgPrimitive[]` / `SvgText[]` for an
|
|
19617
|
+
* existing cartesian chart. No Angular dependencies; all inputs are typed
|
|
19618
|
+
* against `pptx-viewer-core` and the `SvgPrimitive` union already defined in
|
|
19619
|
+
* `chart-renderer-helpers.ts`.
|
|
19620
|
+
*
|
|
19621
|
+
* Ported / adapted from:
|
|
19622
|
+
* packages/react/src/viewer/utils/chart-trendlines.tsx (regression engine)
|
|
19623
|
+
* packages/react/src/viewer/utils/chart-overlay-lines.tsx (error bars)
|
|
19624
|
+
* packages/react/src/viewer/utils/chart-chrome.tsx (axis titles)
|
|
19625
|
+
* packages/react/src/viewer/utils/chart-data-table.tsx (data table)
|
|
19626
|
+
* packages/shared/src/render/chart-trendlines.ts (shared port)
|
|
19627
|
+
*
|
|
19628
|
+
* @module chart-overlays
|
|
19629
|
+
*/
|
|
19630
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19631
|
+
// Internal: coordinate helpers
|
|
19632
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19633
|
+
/**
|
|
19634
|
+
* Map a (possibly fractional / extrapolated) category index to an X pixel.
|
|
19635
|
+
* `mode === 'bar'` centres on category slots; `'line'` anchors at data points.
|
|
19636
|
+
* Mirrors `xToPixel` from chart-overlay-utils.ts (React) and the shared port.
|
|
19637
|
+
*/
|
|
19638
|
+
function xToPixel(xVal, catCount, layout, mode) {
|
|
19639
|
+
if (mode === 'bar') {
|
|
19640
|
+
const slotWidth = layout.plotWidth / Math.max(catCount, 1);
|
|
19641
|
+
return layout.plotLeft + slotWidth * xVal + slotWidth / 2;
|
|
19642
|
+
}
|
|
19643
|
+
const maxIdx = Math.max(catCount - 1, 1);
|
|
19644
|
+
return layout.plotLeft + (xVal / maxIdx) * layout.plotWidth;
|
|
19645
|
+
}
|
|
19646
|
+
/**
|
|
19647
|
+
* Ordinary least-squares linear regression of `yVals` on `xVals`.
|
|
19648
|
+
* Returns slope=0, intercept=mean(y), rSquared=0 when fewer than 2 points or
|
|
19649
|
+
* when the denominator is effectively zero (vertical / constant-x data).
|
|
19650
|
+
*
|
|
19651
|
+
* Mirrors `computeLinearRegression` in chart-trendlines.tsx (React) and
|
|
19652
|
+
* chart-trendlines.ts (shared).
|
|
19653
|
+
*/
|
|
19654
|
+
function computeLinearRegression(xVals, yVals) {
|
|
19655
|
+
const n = xVals.length;
|
|
19656
|
+
if (n < 2) {
|
|
19657
|
+
return { slope: 0, intercept: 0, rSquared: 0 };
|
|
19658
|
+
}
|
|
19659
|
+
let sumX = 0;
|
|
19660
|
+
let sumY = 0;
|
|
19661
|
+
let sumXY = 0;
|
|
19662
|
+
let sumXX = 0;
|
|
19663
|
+
for (let i = 0; i < n; i++) {
|
|
19664
|
+
sumX += xVals[i];
|
|
19665
|
+
sumY += yVals[i];
|
|
19666
|
+
sumXY += xVals[i] * yVals[i];
|
|
19667
|
+
sumXX += xVals[i] * xVals[i];
|
|
19668
|
+
}
|
|
19669
|
+
const denom = n * sumXX - sumX * sumX;
|
|
19670
|
+
if (Math.abs(denom) < 1e-12) {
|
|
19671
|
+
return { slope: 0, intercept: sumY / n, rSquared: 0 };
|
|
19672
|
+
}
|
|
19673
|
+
const slope = (n * sumXY - sumX * sumY) / denom;
|
|
19674
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
19675
|
+
const ssRes = yVals.reduce((s, y, i) => s + (y - (slope * xVals[i] + intercept)) ** 2, 0);
|
|
19676
|
+
const meanY = sumY / n;
|
|
19677
|
+
const ssTot = yVals.reduce((s, y) => s + (y - meanY) ** 2, 0);
|
|
19678
|
+
const rSquared = ssTot > 0 ? 1 - ssRes / ssTot : 0;
|
|
19679
|
+
return { slope, intercept, rSquared };
|
|
19680
|
+
}
|
|
19681
|
+
/**
|
|
19682
|
+
* Fit polynomial coefficients (ascending order: [a0, a1, …, a_order]) via
|
|
19683
|
+
* Gaussian elimination on the normal equations.
|
|
19684
|
+
* Mirrors `fitPolynomial` in chart-trendlines.tsx (React).
|
|
19685
|
+
*/
|
|
19686
|
+
function fitPolynomial(xVals, yVals, order) {
|
|
19687
|
+
const n = xVals.length;
|
|
19688
|
+
const m = order + 1;
|
|
19689
|
+
const matrix = Array.from({ length: m }, () => Array(m + 1).fill(0));
|
|
19690
|
+
for (let i = 0; i < m; i++) {
|
|
19691
|
+
for (let j = 0; j < m; j++) {
|
|
19692
|
+
let sum = 0;
|
|
19693
|
+
for (let k = 0; k < n; k++) {
|
|
19694
|
+
sum += xVals[k] ** (i + j);
|
|
19695
|
+
}
|
|
19696
|
+
matrix[i][j] = sum;
|
|
19697
|
+
}
|
|
19698
|
+
let sum = 0;
|
|
19699
|
+
for (let k = 0; k < n; k++) {
|
|
19700
|
+
sum += yVals[k] * xVals[k] ** i;
|
|
19701
|
+
}
|
|
19702
|
+
matrix[i][m] = sum;
|
|
19703
|
+
}
|
|
19704
|
+
for (let i = 0; i < m; i++) {
|
|
19705
|
+
let maxRow = i;
|
|
19706
|
+
for (let k = i + 1; k < m; k++) {
|
|
19707
|
+
if (Math.abs(matrix[k][i]) > Math.abs(matrix[maxRow][i])) {
|
|
19708
|
+
maxRow = k;
|
|
19709
|
+
}
|
|
19710
|
+
}
|
|
19711
|
+
[matrix[i], matrix[maxRow]] = [matrix[maxRow], matrix[i]];
|
|
19712
|
+
const pivot = matrix[i][i];
|
|
19713
|
+
if (Math.abs(pivot) < 1e-12) {
|
|
19714
|
+
continue;
|
|
19715
|
+
}
|
|
19716
|
+
for (let j = i; j <= m; j++) {
|
|
19717
|
+
matrix[i][j] /= pivot;
|
|
19718
|
+
}
|
|
19719
|
+
for (let k = 0; k < m; k++) {
|
|
19720
|
+
if (k === i) {
|
|
19721
|
+
continue;
|
|
19722
|
+
}
|
|
19723
|
+
const factor = matrix[k][i];
|
|
19724
|
+
for (let j = i; j <= m; j++) {
|
|
19725
|
+
matrix[k][j] -= factor * matrix[i][j];
|
|
19726
|
+
}
|
|
19727
|
+
}
|
|
19728
|
+
}
|
|
19729
|
+
return matrix.map((row) => row[m]);
|
|
19730
|
+
}
|
|
19731
|
+
/**
|
|
19732
|
+
* Coefficient of determination (R²) of an arbitrary fit function against data.
|
|
19733
|
+
* Mirrors `computeRSquared` in chart-trendlines.tsx (React).
|
|
19734
|
+
*/
|
|
19735
|
+
function computeRSquared(xVals, yVals, evalFn) {
|
|
19736
|
+
const n = xVals.length;
|
|
19737
|
+
if (n === 0) {
|
|
19738
|
+
return 0;
|
|
19739
|
+
}
|
|
19740
|
+
const meanY = yVals.reduce((s, y) => s + y, 0) / n;
|
|
19741
|
+
let ssRes = 0;
|
|
19742
|
+
let ssTot = 0;
|
|
19743
|
+
for (let i = 0; i < n; i++) {
|
|
19744
|
+
ssRes += (yVals[i] - evalFn(xVals[i])) ** 2;
|
|
19745
|
+
ssTot += (yVals[i] - meanY) ** 2;
|
|
19746
|
+
}
|
|
19747
|
+
return ssTot > 0 ? 1 - ssRes / ssTot : 0;
|
|
19748
|
+
}
|
|
19749
|
+
/**
|
|
19750
|
+
* Compute the polyline points (and equation / R²) for one trendline over a
|
|
19751
|
+
* series' values. Returns empty points when the type is unsupported or data is
|
|
19752
|
+
* too sparse. Mirrors `computeTrendlinePoints` in chart-trendlines.tsx (React).
|
|
19753
|
+
*/
|
|
19754
|
+
function computeTrendlinePoints(trendline, values, catCount, layout, range, mode) {
|
|
19755
|
+
const n = values.length;
|
|
19756
|
+
if (n < 2) {
|
|
19757
|
+
return { points: [], equation: '', rSquared: 0 };
|
|
19758
|
+
}
|
|
19759
|
+
const xVals = values.map((_v, i) => i);
|
|
19760
|
+
const yVals = values;
|
|
19761
|
+
const forward = trendline.forward ?? 0;
|
|
19762
|
+
const backward = trendline.backward ?? 0;
|
|
19763
|
+
const startX = -backward;
|
|
19764
|
+
const endX = n - 1 + forward;
|
|
19765
|
+
const steps = Math.max(Math.ceil((endX - startX) * 4), 20);
|
|
19766
|
+
let evalFn;
|
|
19767
|
+
let equation = '';
|
|
19768
|
+
let rSquared = 0;
|
|
19769
|
+
switch (trendline.trendlineType) {
|
|
19770
|
+
case 'linear': {
|
|
19771
|
+
const reg = computeLinearRegression(xVals, yVals);
|
|
19772
|
+
const fixedIntercept = trendline.intercept;
|
|
19773
|
+
const slope = fixedIntercept !== undefined
|
|
19774
|
+
? yVals.reduce((s, y, i) => s + (y - fixedIntercept) * xVals[i], 0) /
|
|
19775
|
+
xVals.reduce((s, x) => s + x * x, 0)
|
|
19776
|
+
: reg.slope;
|
|
19777
|
+
const b = fixedIntercept ?? reg.intercept;
|
|
19778
|
+
evalFn = (x) => slope * x + b;
|
|
19779
|
+
equation = `y = ${slope.toFixed(2)}x + ${b.toFixed(2)}`;
|
|
19780
|
+
rSquared = reg.rSquared;
|
|
19781
|
+
break;
|
|
19782
|
+
}
|
|
19783
|
+
case 'exponential': {
|
|
19784
|
+
const posY = yVals.filter((y) => y > 0).map((y) => Math.log(y));
|
|
19785
|
+
const posX = xVals.filter((_x, i) => yVals[i] > 0);
|
|
19786
|
+
if (posY.length < 2) {
|
|
19787
|
+
return { points: [], equation: '', rSquared: 0 };
|
|
19788
|
+
}
|
|
19789
|
+
const reg = computeLinearRegression(posX, posY);
|
|
19790
|
+
const a = Math.exp(reg.intercept);
|
|
19791
|
+
const b = reg.slope;
|
|
19792
|
+
evalFn = (x) => a * Math.exp(b * x);
|
|
19793
|
+
equation = `y = ${a.toFixed(2)}e^(${b.toFixed(2)}x)`;
|
|
19794
|
+
rSquared = reg.rSquared;
|
|
19795
|
+
break;
|
|
19796
|
+
}
|
|
19797
|
+
case 'logarithmic': {
|
|
19798
|
+
const posLnX = xVals.filter((x) => x > 0).map((x) => Math.log(x));
|
|
19799
|
+
const filteredY = yVals.filter((_y, i) => xVals[i] > 0);
|
|
19800
|
+
if (posLnX.length < 2) {
|
|
19801
|
+
return { points: [], equation: '', rSquared: 0 };
|
|
19802
|
+
}
|
|
19803
|
+
const reg = computeLinearRegression(posLnX, filteredY);
|
|
19804
|
+
evalFn = (x) => (x > 0 ? reg.slope * Math.log(x) + reg.intercept : 0);
|
|
19805
|
+
equation = `y = ${reg.slope.toFixed(2)}ln(x) + ${reg.intercept.toFixed(2)}`;
|
|
19806
|
+
rSquared = reg.rSquared;
|
|
19807
|
+
break;
|
|
19808
|
+
}
|
|
19809
|
+
case 'power': {
|
|
19810
|
+
const logXArr = xVals.filter((x, i) => x > 0 && yVals[i] > 0).map((x) => Math.log(x));
|
|
19811
|
+
const logYArr = yVals.filter((y, i) => y > 0 && xVals[i] > 0).map((y) => Math.log(y));
|
|
19812
|
+
if (logXArr.length < 2) {
|
|
19813
|
+
return { points: [], equation: '', rSquared: 0 };
|
|
19814
|
+
}
|
|
19815
|
+
const reg = computeLinearRegression(logXArr, logYArr);
|
|
19816
|
+
const a = Math.exp(reg.intercept);
|
|
19817
|
+
evalFn = (x) => (x > 0 ? a * x ** reg.slope : 0);
|
|
19818
|
+
equation = `y = ${a.toFixed(2)}x^${reg.slope.toFixed(2)}`;
|
|
19819
|
+
rSquared = reg.rSquared;
|
|
19820
|
+
break;
|
|
19821
|
+
}
|
|
19822
|
+
case 'polynomial': {
|
|
19823
|
+
const order = Math.min(trendline.order ?? 2, 6);
|
|
19824
|
+
const coeffs = fitPolynomial(xVals, yVals, order);
|
|
19825
|
+
evalFn = (x) => coeffs.reduce((s, c, i) => s + c * x ** i, 0);
|
|
19826
|
+
equation = coeffs.map((c, i) => `${c.toFixed(2)}x^${i}`).join(' + ');
|
|
19827
|
+
rSquared = computeRSquared(xVals, yVals, evalFn);
|
|
19828
|
+
break;
|
|
19829
|
+
}
|
|
19830
|
+
case 'movingAvg': {
|
|
19831
|
+
const period = trendline.period ?? 2;
|
|
19832
|
+
const maPoints = [];
|
|
19833
|
+
for (let i = period - 1; i < n; i++) {
|
|
19834
|
+
let sum = 0;
|
|
19835
|
+
for (let j = i - period + 1; j <= i; j++) {
|
|
19836
|
+
sum += yVals[j];
|
|
19837
|
+
}
|
|
19838
|
+
const avgVal = sum / period;
|
|
19839
|
+
const px = xToPixel(i, catCount, layout, mode);
|
|
19840
|
+
const py = valueToY(avgVal, range, layout.plotTop, layout.plotBottom);
|
|
19841
|
+
maPoints.push({ x: px, y: py });
|
|
19842
|
+
}
|
|
19843
|
+
return {
|
|
19844
|
+
points: maPoints,
|
|
19845
|
+
equation: `${period}-period moving average`,
|
|
19846
|
+
rSquared: 0,
|
|
19847
|
+
};
|
|
19848
|
+
}
|
|
19849
|
+
default:
|
|
19850
|
+
return { points: [], equation: '', rSquared: 0 };
|
|
19851
|
+
}
|
|
19852
|
+
const points = [];
|
|
19853
|
+
for (let step = 0; step <= steps; step++) {
|
|
19854
|
+
const xVal = startX + ((endX - startX) * step) / steps;
|
|
19855
|
+
const yVal = evalFn(xVal);
|
|
19856
|
+
if (!Number.isFinite(yVal)) {
|
|
19857
|
+
continue;
|
|
19858
|
+
}
|
|
19859
|
+
const px = xToPixel(xVal, catCount, layout, mode);
|
|
19860
|
+
const py = valueToY(yVal, range, layout.plotTop, layout.plotBottom);
|
|
19861
|
+
points.push({ x: px, y: py });
|
|
19862
|
+
}
|
|
19863
|
+
return { points, equation, rSquared };
|
|
19864
|
+
}
|
|
19865
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19866
|
+
// Public: trendline primitives
|
|
19867
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19868
|
+
/**
|
|
19869
|
+
* Build `SvgPrimitive[]` for all trendlines declared by every series in
|
|
19870
|
+
* `chartData`. Returns an empty array when no series declares a trendline.
|
|
19871
|
+
*
|
|
19872
|
+
* Each trendline produces:
|
|
19873
|
+
* - one `SvgPath` (dashed polyline in the series / trendline colour), and
|
|
19874
|
+
* - optionally one `SvgText` with the equation / R² label at the last point.
|
|
19875
|
+
*
|
|
19876
|
+
* @param chartData Full parsed chart data.
|
|
19877
|
+
* @param catCount Number of categories (x-slots), e.g. `chartData.categories.length || 1`.
|
|
19878
|
+
* @param layout Plot-area bounding box from `computePlotLayout`.
|
|
19879
|
+
* @param range Value-axis range from `computeValueRange` / `computeStackedValueRange`.
|
|
19880
|
+
* @param mode `'bar'` for bar/column, `'line'` for line/area/scatter.
|
|
19881
|
+
* @param colorPalette Optional resolved palette (same as passed to `seriesColor`).
|
|
19882
|
+
*/
|
|
19883
|
+
function computeTrendlinePrimitives(chartData, catCount, layout, range, mode = 'line', colorPalette) {
|
|
19884
|
+
const out = [];
|
|
19885
|
+
chartData.series.forEach((series, si) => {
|
|
19886
|
+
if (!series.trendlines || series.trendlines.length === 0) {
|
|
19887
|
+
return;
|
|
19888
|
+
}
|
|
19889
|
+
series.trendlines.forEach((tl) => {
|
|
19890
|
+
const { points, equation, rSquared } = computeTrendlinePoints(tl, series.values, catCount, layout, range, mode);
|
|
19891
|
+
if (points.length < 2) {
|
|
19892
|
+
return;
|
|
19893
|
+
}
|
|
19894
|
+
const pathD = points
|
|
19895
|
+
.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`)
|
|
19896
|
+
.join(' ');
|
|
19897
|
+
const strokeColor = tl.color ?? seriesColor(series, si, colorPalette);
|
|
19898
|
+
const pathPrimitive = {
|
|
19899
|
+
kind: 'path',
|
|
19900
|
+
d: pathD,
|
|
19901
|
+
fill: 'none',
|
|
19902
|
+
stroke: strokeColor,
|
|
19903
|
+
strokeWidth: 1.5,
|
|
19904
|
+
};
|
|
19905
|
+
out.push(pathPrimitive);
|
|
19906
|
+
const labelParts = [];
|
|
19907
|
+
if (tl.displayEq && equation) {
|
|
19908
|
+
labelParts.push(equation);
|
|
19909
|
+
}
|
|
19910
|
+
if (tl.displayRSq) {
|
|
19911
|
+
labelParts.push(`R² = ${rSquared.toFixed(4)}`);
|
|
19912
|
+
}
|
|
19913
|
+
if (labelParts.length > 0) {
|
|
19914
|
+
const last = points[points.length - 1];
|
|
19915
|
+
const labelText = {
|
|
19916
|
+
kind: 'text',
|
|
19917
|
+
x: last.x,
|
|
19918
|
+
y: last.y - 6,
|
|
19919
|
+
text: labelParts.join(' '),
|
|
19920
|
+
fontSize: 7,
|
|
19921
|
+
fill: strokeColor,
|
|
19922
|
+
textAnchor: 'end',
|
|
19923
|
+
};
|
|
19924
|
+
out.push(labelText);
|
|
19925
|
+
}
|
|
19926
|
+
});
|
|
19927
|
+
});
|
|
19928
|
+
return out;
|
|
19929
|
+
}
|
|
19930
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19931
|
+
// Internal: error-value computation
|
|
19932
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19933
|
+
/**
|
|
19934
|
+
* Resolve the half-width of an error bar for a single data point.
|
|
19935
|
+
* Mirrors `computeErrorValue` in chart-overlay-lines.tsx (React).
|
|
19936
|
+
*/
|
|
19937
|
+
function computeErrorValue(errBars, values, pointIndex, direction) {
|
|
19938
|
+
switch (errBars.valType) {
|
|
19939
|
+
case 'fixedVal':
|
|
19940
|
+
return errBars.val ?? 0;
|
|
19941
|
+
case 'percentage':
|
|
19942
|
+
return Math.abs(values[pointIndex]) * ((errBars.val ?? 0) / 100);
|
|
19943
|
+
case 'stdDev': {
|
|
19944
|
+
const n = values.length;
|
|
19945
|
+
if (n === 0) {
|
|
19946
|
+
return 0;
|
|
19947
|
+
}
|
|
19948
|
+
const mean = values.reduce((s, v) => s + v, 0) / n;
|
|
19949
|
+
const variance = values.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
|
|
19950
|
+
return Math.sqrt(variance) * (errBars.val ?? 1);
|
|
19951
|
+
}
|
|
19952
|
+
case 'stdErr': {
|
|
19953
|
+
const n2 = values.length;
|
|
19954
|
+
if (n2 === 0) {
|
|
19955
|
+
return 0;
|
|
19956
|
+
}
|
|
19957
|
+
const mean2 = values.reduce((s, v) => s + v, 0) / n2;
|
|
19958
|
+
const variance2 = values.reduce((s, v) => s + (v - mean2) ** 2, 0) / n2;
|
|
19959
|
+
return Math.sqrt(variance2 / n2);
|
|
19960
|
+
}
|
|
19961
|
+
case 'cust':
|
|
19962
|
+
return direction === 'plus'
|
|
19963
|
+
? (errBars.customPlus?.[pointIndex] ?? 0)
|
|
19964
|
+
: (errBars.customMinus?.[pointIndex] ?? 0);
|
|
19965
|
+
default:
|
|
19966
|
+
return 0;
|
|
19967
|
+
}
|
|
19968
|
+
}
|
|
19969
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19970
|
+
// Public: error-bar primitives
|
|
19971
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19972
|
+
/** Stroke colour for error-bar whiskers. */
|
|
19973
|
+
const ERROR_BAR_COLOR = '#334155';
|
|
19974
|
+
/**
|
|
19975
|
+
* Build `SvgPrimitive[]` for all Y-direction error bars in `chartData`.
|
|
19976
|
+
* Produces stem + cap `SvgLine` pairs for each data point.
|
|
19977
|
+
* X-direction error bars are intentionally skipped (not supported in PPTX
|
|
19978
|
+
* bar/line charts displayed here).
|
|
19979
|
+
*
|
|
19980
|
+
* Mirrors `renderErrorBars` in chart-overlay-lines.tsx (React).
|
|
19981
|
+
*
|
|
19982
|
+
* @param chartData Full parsed chart data.
|
|
19983
|
+
* @param catCount Number of categories.
|
|
19984
|
+
* @param layout Plot-area bounding box.
|
|
19985
|
+
* @param range Value-axis range.
|
|
19986
|
+
* @param mode `'bar'` or `'line'` — controls x-pixel mapping.
|
|
19987
|
+
*/
|
|
19988
|
+
function computeErrorBarPrimitives(chartData, catCount, layout, range, mode = 'line') {
|
|
19989
|
+
const out = [];
|
|
19990
|
+
const capW = 4;
|
|
19991
|
+
chartData.series.forEach((series) => {
|
|
19992
|
+
if (!series.errBars || series.errBars.length === 0) {
|
|
19993
|
+
return;
|
|
19994
|
+
}
|
|
19995
|
+
series.errBars.forEach((eb) => {
|
|
19996
|
+
if (eb.direction !== 'y') {
|
|
19997
|
+
return;
|
|
19998
|
+
}
|
|
19999
|
+
series.values.forEach((val, vi) => {
|
|
20000
|
+
const cx = xToPixel(vi, catCount, layout, mode);
|
|
20001
|
+
const baseY = valueToY(val, range, layout.plotTop, layout.plotBottom);
|
|
20002
|
+
if (eb.barType === 'plus' || eb.barType === 'both') {
|
|
20003
|
+
const plusErr = computeErrorValue(eb, series.values, vi, 'plus');
|
|
20004
|
+
const topY = valueToY(val + plusErr, range, layout.plotTop, layout.plotBottom);
|
|
20005
|
+
const stem = {
|
|
20006
|
+
kind: 'line',
|
|
20007
|
+
x1: cx,
|
|
20008
|
+
y1: baseY,
|
|
20009
|
+
x2: cx,
|
|
20010
|
+
y2: topY,
|
|
20011
|
+
stroke: ERROR_BAR_COLOR,
|
|
20012
|
+
strokeWidth: 1,
|
|
20013
|
+
};
|
|
20014
|
+
const cap = {
|
|
20015
|
+
kind: 'line',
|
|
20016
|
+
x1: cx - capW,
|
|
20017
|
+
y1: topY,
|
|
20018
|
+
x2: cx + capW,
|
|
20019
|
+
y2: topY,
|
|
20020
|
+
stroke: ERROR_BAR_COLOR,
|
|
20021
|
+
strokeWidth: 1,
|
|
20022
|
+
};
|
|
20023
|
+
out.push(stem, cap);
|
|
20024
|
+
}
|
|
20025
|
+
if (eb.barType === 'minus' || eb.barType === 'both') {
|
|
20026
|
+
const minusErr = computeErrorValue(eb, series.values, vi, 'minus');
|
|
20027
|
+
const botY = valueToY(val - minusErr, range, layout.plotTop, layout.plotBottom);
|
|
20028
|
+
const stem = {
|
|
20029
|
+
kind: 'line',
|
|
20030
|
+
x1: cx,
|
|
20031
|
+
y1: baseY,
|
|
20032
|
+
x2: cx,
|
|
20033
|
+
y2: botY,
|
|
20034
|
+
stroke: ERROR_BAR_COLOR,
|
|
20035
|
+
strokeWidth: 1,
|
|
20036
|
+
};
|
|
20037
|
+
const cap = {
|
|
20038
|
+
kind: 'line',
|
|
20039
|
+
x1: cx - capW,
|
|
20040
|
+
y1: botY,
|
|
20041
|
+
x2: cx + capW,
|
|
20042
|
+
y2: botY,
|
|
20043
|
+
stroke: ERROR_BAR_COLOR,
|
|
20044
|
+
strokeWidth: 1,
|
|
20045
|
+
};
|
|
20046
|
+
out.push(stem, cap);
|
|
20047
|
+
}
|
|
20048
|
+
});
|
|
20049
|
+
});
|
|
20050
|
+
});
|
|
20051
|
+
return out;
|
|
20052
|
+
}
|
|
20053
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20054
|
+
// Public: axis title primitives
|
|
20055
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20056
|
+
/** Fill colour for axis title text. */
|
|
20057
|
+
const AXIS_TITLE_COLOR = '#475569';
|
|
20058
|
+
/**
|
|
20059
|
+
* Build `SvgText[]` for the X and Y axis titles.
|
|
20060
|
+
*
|
|
20061
|
+
* Axis titles are read from `chartData.axes`:
|
|
20062
|
+
* - the primary category axis (`catAx`, `axPos === 'b'`) drives the X title
|
|
20063
|
+
* - the primary value axis (`valAx`, `axPos === 'l'` or first `valAx`) drives the Y title
|
|
20064
|
+
*
|
|
20065
|
+
* **Rotation note**: `SvgText` has no `transform` or `rotate` field. The Y
|
|
20066
|
+
* axis title is therefore placed to the left of the plot without rotation and
|
|
20067
|
+
* noted inline. If the orchestrator adds a `transform?: string` field to
|
|
20068
|
+
* `SvgText` (or a new `SvgTransform` wrapper primitive), the Y title can be
|
|
20069
|
+
* rendered rotated -90° by passing
|
|
20070
|
+
* `transform: \`rotate(-90, ${x}, ${y})\`` — the template expression is
|
|
20071
|
+
* straightforward once the field exists.
|
|
20072
|
+
*
|
|
20073
|
+
* @param chartData Full parsed chart data.
|
|
20074
|
+
* @param layout Plot-area bounding box.
|
|
20075
|
+
*/
|
|
20076
|
+
function computeAxisTitlePrimitives(chartData, layout) {
|
|
20077
|
+
const out = [];
|
|
20078
|
+
const axes = chartData.axes;
|
|
20079
|
+
if (!axes || axes.length === 0) {
|
|
20080
|
+
return out;
|
|
20081
|
+
}
|
|
20082
|
+
// X axis title (category axis at bottom).
|
|
20083
|
+
const catAxis = axes.find((a) => a.axisType === 'catAx' && a.axPos !== 'r' && a.titleText);
|
|
20084
|
+
if (catAxis?.titleText) {
|
|
20085
|
+
const xTitle = {
|
|
20086
|
+
kind: 'text',
|
|
20087
|
+
x: layout.plotLeft + layout.plotWidth / 2,
|
|
20088
|
+
y: layout.plotBottom + 22,
|
|
20089
|
+
text: catAxis.titleText,
|
|
20090
|
+
fontSize: 9,
|
|
20091
|
+
fill: AXIS_TITLE_COLOR,
|
|
20092
|
+
textAnchor: 'middle',
|
|
20093
|
+
fontWeight: 'bold',
|
|
20094
|
+
};
|
|
20095
|
+
out.push(xTitle);
|
|
20096
|
+
}
|
|
20097
|
+
// Y axis title (value axis at left), rotated -90° about its own anchor and
|
|
20098
|
+
// centred vertically on the plot area.
|
|
20099
|
+
const valAxis = axes.find((a) => a.axisType === 'valAx' && a.axPos !== 'r' && a.titleText) ??
|
|
20100
|
+
axes.find((a) => a.axisType === 'valAx' && a.titleText);
|
|
20101
|
+
if (valAxis?.titleText) {
|
|
20102
|
+
const yx = 12;
|
|
20103
|
+
const yy = layout.plotTop + layout.plotHeight / 2;
|
|
20104
|
+
const yTitle = {
|
|
20105
|
+
kind: 'text',
|
|
20106
|
+
x: yx,
|
|
20107
|
+
y: yy,
|
|
20108
|
+
text: valAxis.titleText,
|
|
20109
|
+
fontSize: 9,
|
|
20110
|
+
fill: AXIS_TITLE_COLOR,
|
|
20111
|
+
textAnchor: 'middle',
|
|
20112
|
+
fontWeight: 'bold',
|
|
20113
|
+
transform: `rotate(-90, ${yx}, ${yy})`,
|
|
20114
|
+
};
|
|
20115
|
+
out.push(yTitle);
|
|
20116
|
+
}
|
|
20117
|
+
return out;
|
|
20118
|
+
}
|
|
20119
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20120
|
+
// Internal: data-table cell formatting
|
|
20121
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20122
|
+
/**
|
|
20123
|
+
* Format a single data value as a short human-readable string.
|
|
20124
|
+
* Mirrors `formatValue` in chart-data-table.tsx (React) — identical to
|
|
20125
|
+
* `formatAxisValue` but kept local for clarity.
|
|
20126
|
+
*/
|
|
20127
|
+
function formatDataValue(val) {
|
|
20128
|
+
return formatAxisValue(val);
|
|
20129
|
+
}
|
|
20130
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20131
|
+
// Public: data table primitives
|
|
20132
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20133
|
+
/**
|
|
20134
|
+
* Layout constants for the SVG data table rendered below the plot area.
|
|
20135
|
+
* Kept as named constants so tests can assert against them without magic numbers.
|
|
20136
|
+
*/
|
|
20137
|
+
const DATA_TABLE_ROW_H = 14;
|
|
20138
|
+
const DATA_TABLE_HEADER_H = 14;
|
|
20139
|
+
const DATA_TABLE_KEY_W = 60;
|
|
20140
|
+
const DATA_TABLE_PADDING = 4;
|
|
20141
|
+
/**
|
|
20142
|
+
* Build `SvgPrimitive[]` for a simple data table rendered below the plot area.
|
|
20143
|
+
*
|
|
20144
|
+
* The table is rendered as SVG `rect` (borders) + `text` (labels) primitives.
|
|
20145
|
+
* Columns = categories; rows = series (with an optional series-key column on
|
|
20146
|
+
* the left when `dataTable.showKeys !== false`).
|
|
20147
|
+
*
|
|
20148
|
+
* Border flags from `PptxChartDataTable` are respected:
|
|
20149
|
+
* - `showHorzBorder` — horizontal rules between rows
|
|
20150
|
+
* - `showVertBorder` — vertical rules between columns
|
|
20151
|
+
* - `showOutline` — outer border rectangle
|
|
20152
|
+
* - `showKeys` — series name/colour key column
|
|
20153
|
+
*
|
|
20154
|
+
* Mirrors `renderChartDataTable` in chart-data-table.tsx (React), translated
|
|
20155
|
+
* to pure SVG primitives so the Angular template can render them natively.
|
|
20156
|
+
*
|
|
20157
|
+
* @param chartData Full parsed chart data (`dataTable` must be present).
|
|
20158
|
+
* @param layout Plot-area bounding box — the table is placed at `plotBottom + 4`.
|
|
20159
|
+
* @param colorPalette Optional resolved colour palette (same as chart).
|
|
20160
|
+
*/
|
|
20161
|
+
function computeDataTablePrimitives(chartData, layout, colorPalette) {
|
|
20162
|
+
const table = chartData.dataTable;
|
|
20163
|
+
if (!table) {
|
|
20164
|
+
return [];
|
|
20165
|
+
}
|
|
20166
|
+
const categories = chartData.categories;
|
|
20167
|
+
const series = chartData.series;
|
|
20168
|
+
if (categories.length === 0 && series.length === 0) {
|
|
20169
|
+
return [];
|
|
20170
|
+
}
|
|
20171
|
+
const out = [];
|
|
20172
|
+
const showH = table.showHorzBorder !== false;
|
|
20173
|
+
const showV = table.showVertBorder !== false;
|
|
20174
|
+
const showO = table.showOutline !== false;
|
|
20175
|
+
const showK = table.showKeys !== false;
|
|
20176
|
+
const borderColor = '#cbd5e1';
|
|
20177
|
+
const textColor = '#334155';
|
|
20178
|
+
const headerWeight = 'bold';
|
|
20179
|
+
const catCount = categories.length;
|
|
20180
|
+
const seriesCount = series.length;
|
|
20181
|
+
// Column metrics
|
|
20182
|
+
const keyColW = showK ? DATA_TABLE_KEY_W : 0;
|
|
20183
|
+
const totalW = layout.plotWidth;
|
|
20184
|
+
const cellW = catCount > 0 ? (totalW - keyColW) / catCount : totalW - keyColW;
|
|
20185
|
+
// Table top edge (just below the plot bottom)
|
|
20186
|
+
const tableTop = layout.plotBottom + DATA_TABLE_PADDING;
|
|
20187
|
+
// Total table height: 1 header row + N series rows
|
|
20188
|
+
const tableH = DATA_TABLE_HEADER_H + seriesCount * DATA_TABLE_ROW_H;
|
|
20189
|
+
// Outer border — rendered as four SvgLine segments because SvgRect has no
|
|
20190
|
+
// `stroke` field (only `fill`).
|
|
20191
|
+
if (showO) {
|
|
20192
|
+
const mkBorderLine = (x1, y1, x2, y2) => ({
|
|
20193
|
+
kind: 'line',
|
|
20194
|
+
x1,
|
|
20195
|
+
y1,
|
|
20196
|
+
x2,
|
|
20197
|
+
y2,
|
|
20198
|
+
stroke: borderColor,
|
|
20199
|
+
strokeWidth: 1,
|
|
20200
|
+
});
|
|
20201
|
+
out.push(mkBorderLine(layout.plotLeft, tableTop, layout.plotLeft + totalW, tableTop));
|
|
20202
|
+
out.push(mkBorderLine(layout.plotLeft + totalW, tableTop, layout.plotLeft + totalW, tableTop + tableH));
|
|
20203
|
+
out.push(mkBorderLine(layout.plotLeft + totalW, tableTop + tableH, layout.plotLeft, tableTop + tableH));
|
|
20204
|
+
out.push(mkBorderLine(layout.plotLeft, tableTop + tableH, layout.plotLeft, tableTop));
|
|
20205
|
+
}
|
|
20206
|
+
// Helper: x-position of column ci (0-based category columns, after key col)
|
|
20207
|
+
function colX(ci) {
|
|
20208
|
+
return layout.plotLeft + keyColW + ci * cellW;
|
|
20209
|
+
}
|
|
20210
|
+
// Helper: y-position of row ri (0 = header)
|
|
20211
|
+
function rowY(ri) {
|
|
20212
|
+
return tableTop + (ri === 0 ? 0 : DATA_TABLE_HEADER_H + (ri - 1) * DATA_TABLE_ROW_H);
|
|
20213
|
+
}
|
|
20214
|
+
// Header row: category labels
|
|
20215
|
+
categories.forEach((cat, ci) => {
|
|
20216
|
+
const x = colX(ci) + cellW / 2;
|
|
20217
|
+
const y = rowY(0) + DATA_TABLE_HEADER_H / 2 + 3;
|
|
20218
|
+
const label = {
|
|
20219
|
+
kind: 'text',
|
|
20220
|
+
x,
|
|
20221
|
+
y,
|
|
20222
|
+
text: cat,
|
|
20223
|
+
fontSize: 8,
|
|
20224
|
+
fill: textColor,
|
|
20225
|
+
textAnchor: 'middle',
|
|
20226
|
+
fontWeight: headerWeight,
|
|
20227
|
+
};
|
|
20228
|
+
out.push(label);
|
|
20229
|
+
// Vertical border after this column header (not after the last)
|
|
20230
|
+
if (showV && ci < catCount - 1) {
|
|
20231
|
+
const vx = colX(ci) + cellW;
|
|
20232
|
+
const vLine = {
|
|
20233
|
+
kind: 'line',
|
|
20234
|
+
x1: vx,
|
|
20235
|
+
y1: tableTop,
|
|
20236
|
+
x2: vx,
|
|
20237
|
+
y2: tableTop + tableH,
|
|
20238
|
+
stroke: borderColor,
|
|
20239
|
+
strokeWidth: 1,
|
|
20240
|
+
};
|
|
20241
|
+
out.push(vLine);
|
|
20242
|
+
}
|
|
20243
|
+
});
|
|
20244
|
+
// Horizontal border under header
|
|
20245
|
+
if (showH) {
|
|
20246
|
+
const hy = tableTop + DATA_TABLE_HEADER_H;
|
|
20247
|
+
const hLine = {
|
|
20248
|
+
kind: 'line',
|
|
20249
|
+
x1: layout.plotLeft,
|
|
20250
|
+
y1: hy,
|
|
20251
|
+
x2: layout.plotLeft + totalW,
|
|
20252
|
+
y2: hy,
|
|
20253
|
+
stroke: borderColor,
|
|
20254
|
+
strokeWidth: 1,
|
|
20255
|
+
};
|
|
20256
|
+
out.push(hLine);
|
|
20257
|
+
}
|
|
20258
|
+
// Vertical border between key column and first data column
|
|
20259
|
+
if (showK && showV) {
|
|
20260
|
+
const kvx = layout.plotLeft + keyColW;
|
|
20261
|
+
const kvLine = {
|
|
20262
|
+
kind: 'line',
|
|
20263
|
+
x1: kvx,
|
|
20264
|
+
y1: tableTop,
|
|
20265
|
+
x2: kvx,
|
|
20266
|
+
y2: tableTop + tableH,
|
|
20267
|
+
stroke: borderColor,
|
|
20268
|
+
strokeWidth: 1,
|
|
20269
|
+
};
|
|
20270
|
+
out.push(kvLine);
|
|
20271
|
+
}
|
|
20272
|
+
// Data rows
|
|
20273
|
+
series.forEach((s, si) => {
|
|
20274
|
+
const rowIndex = si + 1; // row 0 is the header
|
|
20275
|
+
const ry = rowY(rowIndex);
|
|
20276
|
+
const cellCy = ry + DATA_TABLE_ROW_H / 2 + 3;
|
|
20277
|
+
// Series key cell (colour swatch + name)
|
|
20278
|
+
if (showK) {
|
|
20279
|
+
const swatchX = layout.plotLeft + DATA_TABLE_PADDING;
|
|
20280
|
+
const swatchY = ry + DATA_TABLE_ROW_H / 2 - 3;
|
|
20281
|
+
const swatchColor = seriesColor(s, si, colorPalette);
|
|
20282
|
+
// Colour swatch as a small filled rect
|
|
20283
|
+
out.push({
|
|
20284
|
+
kind: 'rect',
|
|
20285
|
+
x: swatchX,
|
|
20286
|
+
y: swatchY,
|
|
20287
|
+
w: 7,
|
|
20288
|
+
h: 7,
|
|
20289
|
+
fill: swatchColor,
|
|
20290
|
+
rx: 1,
|
|
20291
|
+
});
|
|
20292
|
+
// Series name text
|
|
20293
|
+
const nameX = swatchX + 9;
|
|
20294
|
+
const nameLabel = {
|
|
20295
|
+
kind: 'text',
|
|
20296
|
+
x: nameX,
|
|
20297
|
+
y: cellCy,
|
|
20298
|
+
text: s.name,
|
|
20299
|
+
fontSize: 8,
|
|
20300
|
+
fill: textColor,
|
|
20301
|
+
textAnchor: 'start',
|
|
20302
|
+
};
|
|
20303
|
+
out.push(nameLabel);
|
|
20304
|
+
}
|
|
20305
|
+
// Data cells
|
|
20306
|
+
categories.forEach((_cat, ci) => {
|
|
20307
|
+
const val = s.values[ci];
|
|
20308
|
+
const cellLabel = {
|
|
20309
|
+
kind: 'text',
|
|
20310
|
+
x: colX(ci) + cellW / 2,
|
|
20311
|
+
y: cellCy,
|
|
20312
|
+
text: val !== undefined ? formatDataValue(val) : '',
|
|
20313
|
+
fontSize: 8,
|
|
20314
|
+
fill: textColor,
|
|
20315
|
+
textAnchor: 'middle',
|
|
20316
|
+
};
|
|
20317
|
+
out.push(cellLabel);
|
|
20318
|
+
});
|
|
20319
|
+
// Horizontal border below this row (not after the last)
|
|
20320
|
+
if (showH && si < seriesCount - 1) {
|
|
20321
|
+
const hy2 = ry + DATA_TABLE_ROW_H;
|
|
20322
|
+
const hRowLine = {
|
|
20323
|
+
kind: 'line',
|
|
20324
|
+
x1: layout.plotLeft,
|
|
20325
|
+
y1: hy2,
|
|
20326
|
+
x2: layout.plotLeft + totalW,
|
|
20327
|
+
y2: hy2,
|
|
20328
|
+
stroke: borderColor,
|
|
20329
|
+
strokeWidth: 1,
|
|
20330
|
+
};
|
|
20331
|
+
out.push(hRowLine);
|
|
20332
|
+
}
|
|
20333
|
+
});
|
|
20334
|
+
return out;
|
|
20335
|
+
}
|
|
20336
|
+
|
|
19613
20337
|
/**
|
|
19614
20338
|
* View-model builders for surface and treemap chart kinds.
|
|
19615
20339
|
*
|
|
@@ -21409,6 +22133,9 @@ function buildCartesianViewModel(element, chartData, categoryLabels, kind) {
|
|
|
21409
22133
|
}
|
|
21410
22134
|
}
|
|
21411
22135
|
const title = chartData.style?.hasTitle && chartData.title ? chartData.title : undefined;
|
|
22136
|
+
// Overlays (depth): regression trendlines, error bars, axis titles, data
|
|
22137
|
+
// table — appended on top of the base cartesian primitives.
|
|
22138
|
+
primitives.push(...computeTrendlinePrimitives(chartData, catCount, layout, range, catAxisStyle, chartData.colorPalette), ...computeErrorBarPrimitives(chartData, catCount, layout, range, catAxisStyle), ...computeAxisTitlePrimitives(chartData, layout), ...computeDataTablePrimitives(chartData, layout, chartData.colorPalette));
|
|
21412
22139
|
return {
|
|
21413
22140
|
svgWidth: layout.svgWidth,
|
|
21414
22141
|
svgHeight: layout.svgHeight,
|
|
@@ -21751,6 +22478,21 @@ class ChartRendererComponent {
|
|
|
21751
22478
|
[attr.stroke-dasharray]="asPolygon(prim).dashArray ?? null"
|
|
21752
22479
|
/>
|
|
21753
22480
|
}
|
|
22481
|
+
@case ('text') {
|
|
22482
|
+
<text
|
|
22483
|
+
[attr.x]="asText(prim).x"
|
|
22484
|
+
[attr.y]="asText(prim).y"
|
|
22485
|
+
[attr.text-anchor]="asText(prim).textAnchor"
|
|
22486
|
+
[attr.font-size]="asText(prim).fontSize"
|
|
22487
|
+
[attr.fill]="asText(prim).fill"
|
|
22488
|
+
[attr.font-weight]="asText(prim).fontWeight ?? 'normal'"
|
|
22489
|
+
[attr.dominant-baseline]="asText(prim).dominantBaseline ?? 'auto'"
|
|
22490
|
+
[attr.opacity]="asText(prim).opacity ?? 1"
|
|
22491
|
+
[attr.transform]="asText(prim).transform ?? null"
|
|
22492
|
+
>
|
|
22493
|
+
{{ asText(prim).text }}
|
|
22494
|
+
</text>
|
|
22495
|
+
}
|
|
21754
22496
|
}
|
|
21755
22497
|
}
|
|
21756
22498
|
|
|
@@ -21940,6 +22682,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
21940
22682
|
[attr.stroke-dasharray]="asPolygon(prim).dashArray ?? null"
|
|
21941
22683
|
/>
|
|
21942
22684
|
}
|
|
22685
|
+
@case ('text') {
|
|
22686
|
+
<text
|
|
22687
|
+
[attr.x]="asText(prim).x"
|
|
22688
|
+
[attr.y]="asText(prim).y"
|
|
22689
|
+
[attr.text-anchor]="asText(prim).textAnchor"
|
|
22690
|
+
[attr.font-size]="asText(prim).fontSize"
|
|
22691
|
+
[attr.fill]="asText(prim).fill"
|
|
22692
|
+
[attr.font-weight]="asText(prim).fontWeight ?? 'normal'"
|
|
22693
|
+
[attr.dominant-baseline]="asText(prim).dominantBaseline ?? 'auto'"
|
|
22694
|
+
[attr.opacity]="asText(prim).opacity ?? 1"
|
|
22695
|
+
[attr.transform]="asText(prim).transform ?? null"
|
|
22696
|
+
>
|
|
22697
|
+
{{ asText(prim).text }}
|
|
22698
|
+
</text>
|
|
22699
|
+
}
|
|
21943
22700
|
}
|
|
21944
22701
|
}
|
|
21945
22702
|
|
|
@@ -27653,6 +28410,855 @@ function bulletIndentPx(level) {
|
|
|
27653
28410
|
return lvl * INDENT_PX_PER_LEVEL;
|
|
27654
28411
|
}
|
|
27655
28412
|
|
|
28413
|
+
/**
|
|
28414
|
+
* Presets that require SVG `<textPath>` rendering along a curved/circular path.
|
|
28415
|
+
* All other warp presets (envelope: inflate/deflate/can; simple: slant/fade/
|
|
28416
|
+
* cascade) are rendered with CSS transforms instead — see `text-warp.ts`'s
|
|
28417
|
+
* `getWarpCategory`, which this set must stay in sync with (the `'path'`
|
|
28418
|
+
* category). Listing a CSS preset here would wrongly route it to `<textPath>`.
|
|
28419
|
+
*/
|
|
28420
|
+
const SVG_WARP_PRESETS = new Set([
|
|
28421
|
+
'textArchUp',
|
|
28422
|
+
'textArchDown',
|
|
28423
|
+
'textCircle',
|
|
28424
|
+
'textWave1',
|
|
28425
|
+
'textWave2',
|
|
28426
|
+
'textWave4',
|
|
28427
|
+
'textDoubleWave1',
|
|
28428
|
+
'textCurveUp',
|
|
28429
|
+
'textCurveDown',
|
|
28430
|
+
'textArchUpPour',
|
|
28431
|
+
'textArchDownPour',
|
|
28432
|
+
'textCirclePour',
|
|
28433
|
+
'textButton',
|
|
28434
|
+
'textButtonPour',
|
|
28435
|
+
'textRingInside',
|
|
28436
|
+
'textRingOutside',
|
|
28437
|
+
'textTriangle',
|
|
28438
|
+
'textTriangleInverted',
|
|
28439
|
+
'textChevron',
|
|
28440
|
+
'textChevronInverted',
|
|
28441
|
+
'textStop',
|
|
28442
|
+
]);
|
|
28443
|
+
// ── Cascade helpers (inlined from warp-path-cascade) ──────────────────
|
|
28444
|
+
/** Cascading up — lines tilt from lower-left to upper-right.
|
|
28445
|
+
* adj (default 44444) controls the cascade tilt amount. */
|
|
28446
|
+
function cascadeUpPath(w, h, t, adj) {
|
|
28447
|
+
const adjFactor = adj !== undefined ? adj / 44444 : 1;
|
|
28448
|
+
const tilt = 0.2 * Math.max(0, Math.min(adjFactor, 4));
|
|
28449
|
+
const yMid = h * (0.2 + t * 0.6);
|
|
28450
|
+
const yStart = yMid + (h * tilt) / 2;
|
|
28451
|
+
const yEnd = yMid - (h * tilt) / 2;
|
|
28452
|
+
return `M 0,${yStart} L ${w},${yEnd}`;
|
|
28453
|
+
}
|
|
28454
|
+
/** Cascading down — lines tilt from upper-left to lower-right.
|
|
28455
|
+
* adj (default 44444) controls the cascade tilt amount. */
|
|
28456
|
+
function cascadeDownPath(w, h, t, adj) {
|
|
28457
|
+
const adjFactor = adj !== undefined ? adj / 44444 : 1;
|
|
28458
|
+
const tilt = 0.2 * Math.max(0, Math.min(adjFactor, 4));
|
|
28459
|
+
const yMid = h * (0.2 + t * 0.6);
|
|
28460
|
+
const yStart = yMid - (h * tilt) / 2;
|
|
28461
|
+
const yEnd = yMid + (h * tilt) / 2;
|
|
28462
|
+
return `M 0,${yStart} L ${w},${yEnd}`;
|
|
28463
|
+
}
|
|
28464
|
+
// ── Priority 1 path generators ─────────────────────────────────────────
|
|
28465
|
+
/** Concentric upward arcs from (0, h) → (w, h). t=0 is the tallest arch.
|
|
28466
|
+
* adj (default 10800000) controls the arch height — higher values = taller arch. */
|
|
28467
|
+
function archUpPath(w, h, t, adj) {
|
|
28468
|
+
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
28469
|
+
const maxArch = (0.85 * adjNorm) / 0.5;
|
|
28470
|
+
const archH = h * Math.max(0, maxArch - t * 0.7);
|
|
28471
|
+
if (archH < 1) {
|
|
28472
|
+
return `M 0,${h} L ${w},${h}`;
|
|
28473
|
+
}
|
|
28474
|
+
return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
|
|
28475
|
+
}
|
|
28476
|
+
/** Concentric downward arcs from (0, 0) → (w, 0). t=1 is the deepest.
|
|
28477
|
+
* adj (default 10800000) controls the arch depth. */
|
|
28478
|
+
function archDownPath(w, h, t, adj) {
|
|
28479
|
+
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
28480
|
+
const baseDepth = (0.15 * adjNorm) / 0.5;
|
|
28481
|
+
const archH = h * (baseDepth + t * 0.7);
|
|
28482
|
+
if (archH < 1) {
|
|
28483
|
+
return `M 0,0 L ${w},0`;
|
|
28484
|
+
}
|
|
28485
|
+
return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
|
|
28486
|
+
}
|
|
28487
|
+
/** Full ellipse — concentric ellipses shrink towards centre.
|
|
28488
|
+
* adj (default 10800000) controls the arc span angle in 60000ths of a degree. */
|
|
28489
|
+
function circlePath(w, h, t, adj) {
|
|
28490
|
+
const cx = w / 2;
|
|
28491
|
+
const cy = h / 2;
|
|
28492
|
+
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
28493
|
+
const baseScale = 0.45 + adjNorm * 1.1;
|
|
28494
|
+
const scale = Math.min(1, baseScale) - t * 0.55;
|
|
28495
|
+
const rx = Math.max(1, (w / 2) * scale);
|
|
28496
|
+
const ry = Math.max(1, (h / 2) * scale);
|
|
28497
|
+
return (`M ${cx},${cy - ry} ` +
|
|
28498
|
+
`A ${rx},${ry} 0 1,1 ${cx},${cy + ry} ` +
|
|
28499
|
+
`A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
|
|
28500
|
+
}
|
|
28501
|
+
/** Single sine-wave from left to right using a cubic Bézier.
|
|
28502
|
+
* adj (default 12500) controls wave amplitude.
|
|
28503
|
+
* adj2 (default 0) controls horizontal phase shift. */
|
|
28504
|
+
function wave1Path(w, h, t, adj, adj2) {
|
|
28505
|
+
const yMid = h * (0.25 + t * 0.5);
|
|
28506
|
+
const adjFactor = adj !== undefined ? adj / 12500 : 1;
|
|
28507
|
+
const amp = h * 0.2 * Math.max(0, Math.min(adjFactor, 4));
|
|
28508
|
+
const hShift = adj2 !== undefined ? (adj2 / 100000) * w * 0.3 : 0;
|
|
28509
|
+
const cp1x = w / 3 + hShift;
|
|
28510
|
+
const cp2x = (2 * w) / 3 + hShift;
|
|
28511
|
+
return `M 0,${yMid} C ${cp1x},${yMid - amp} ${cp2x},${yMid + amp} ${w},${yMid}`;
|
|
28512
|
+
}
|
|
28513
|
+
/** Inflate — top lines bow upward, bottom lines bow downward.
|
|
28514
|
+
* adj (default 18750) controls bulge amount. */
|
|
28515
|
+
function inflatePath(w, h, t, adj) {
|
|
28516
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28517
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28518
|
+
const bulge = h * 0.3 * (1 - 2 * t) * Math.max(0, Math.min(adjFactor, 4));
|
|
28519
|
+
return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
|
|
28520
|
+
}
|
|
28521
|
+
/** Deflate — opposite of inflate.
|
|
28522
|
+
* adj (default 18750) controls pinch amount. */
|
|
28523
|
+
function deflatePath(w, h, t, adj) {
|
|
28524
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28525
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28526
|
+
const pinch = h * 0.3 * (2 * t - 1) * Math.max(0, Math.min(adjFactor, 4));
|
|
28527
|
+
return `M 0,${yBase} Q ${w / 2},${yBase - pinch} ${w},${yBase}`;
|
|
28528
|
+
}
|
|
28529
|
+
/** Gentle upward curve.
|
|
28530
|
+
* adj (default 45977) controls curve height. */
|
|
28531
|
+
function curveUpPath(w, h, t, adj) {
|
|
28532
|
+
const yBase = h * (0.35 + t * 0.55);
|
|
28533
|
+
const adjFactor = adj !== undefined ? adj / 45977 : 1;
|
|
28534
|
+
const curve = h * 0.4 * (1 - t * 0.3) * Math.max(0, Math.min(adjFactor, 4));
|
|
28535
|
+
return `M 0,${yBase} Q ${w / 2},${yBase - curve} ${w},${yBase}`;
|
|
28536
|
+
}
|
|
28537
|
+
/** Gentle downward curve.
|
|
28538
|
+
* adj (default 45977) controls curve depth. */
|
|
28539
|
+
function curveDownPath(w, h, t, adj) {
|
|
28540
|
+
const yBase = h * (0.1 + t * 0.55);
|
|
28541
|
+
const adjFactor = adj !== undefined ? adj / 45977 : 1;
|
|
28542
|
+
const curve = h * 0.4 * (1 - (1 - t) * 0.3) * Math.max(0, Math.min(adjFactor, 4));
|
|
28543
|
+
return `M 0,${yBase} Q ${w / 2},${yBase + curve} ${w},${yBase}`;
|
|
28544
|
+
}
|
|
28545
|
+
// ── Priority 2 path generators ─────────────────────────────────────────
|
|
28546
|
+
/** Inverted single wave (phase-shifted wave1).
|
|
28547
|
+
* adj (default 12500) controls wave amplitude.
|
|
28548
|
+
* adj2 (default 0) controls horizontal phase shift. */
|
|
28549
|
+
function wave2Path(w, h, t, adj, adj2) {
|
|
28550
|
+
const yMid = h * (0.25 + t * 0.5);
|
|
28551
|
+
const adjFactor = adj !== undefined ? adj / 12500 : 1;
|
|
28552
|
+
const amp = h * 0.2 * Math.max(0, Math.min(adjFactor, 4));
|
|
28553
|
+
const hShift = adj2 !== undefined ? (adj2 / 100000) * w * 0.3 : 0;
|
|
28554
|
+
const cp1x = w / 3 + hShift;
|
|
28555
|
+
const cp2x = (2 * w) / 3 + hShift;
|
|
28556
|
+
return `M 0,${yMid} C ${cp1x},${yMid + amp} ${cp2x},${yMid - amp} ${w},${yMid}`;
|
|
28557
|
+
}
|
|
28558
|
+
/** Double wave — two full wave cycles across the width.
|
|
28559
|
+
* adj (default 12500) controls wave amplitude.
|
|
28560
|
+
* adj2 (default 0) controls horizontal phase shift. */
|
|
28561
|
+
function wave4Path(w, h, t, adj, adj2) {
|
|
28562
|
+
const yMid = h * (0.25 + t * 0.5);
|
|
28563
|
+
const adjFactor = adj !== undefined ? adj / 12500 : 1;
|
|
28564
|
+
const amp = h * 0.15 * Math.max(0, Math.min(adjFactor, 4));
|
|
28565
|
+
const hShift = adj2 !== undefined ? (adj2 / 100000) * w * 0.15 : 0;
|
|
28566
|
+
const q = w / 4;
|
|
28567
|
+
return (`M 0,${yMid} ` +
|
|
28568
|
+
`C ${q + hShift},${yMid - amp} ${2 * q + hShift},${yMid + amp} ${w / 2},${yMid} ` +
|
|
28569
|
+
`C ${w / 2 + q + hShift},${yMid - amp} ${w - q + hShift},${yMid + amp} ${w},${yMid}`);
|
|
28570
|
+
}
|
|
28571
|
+
/** Double wave with alternating rhythm.
|
|
28572
|
+
* adj (default 6250) controls wave amplitude.
|
|
28573
|
+
* adj2 (default 0) controls horizontal phase shift. */
|
|
28574
|
+
function doubleWave1Path(w, h, t, adj, adj2) {
|
|
28575
|
+
const yMid = h * (0.25 + t * 0.5);
|
|
28576
|
+
const adjFactor = adj !== undefined ? adj / 6250 : 1;
|
|
28577
|
+
const amp = h * 0.18 * Math.max(0, Math.min(adjFactor, 4));
|
|
28578
|
+
const hShift = adj2 !== undefined ? (adj2 / 100000) * w * 0.15 : 0;
|
|
28579
|
+
const q = w / 4;
|
|
28580
|
+
return (`M 0,${yMid} ` +
|
|
28581
|
+
`C ${q + hShift},${yMid - amp} ${2 * q + hShift},${yMid + amp} ${w / 2},${yMid} ` +
|
|
28582
|
+
`C ${w / 2 + q + hShift},${yMid + amp} ${w - q + hShift},${yMid - amp} ${w},${yMid}`);
|
|
28583
|
+
}
|
|
28584
|
+
/** Cylindrical text — upward.
|
|
28585
|
+
* adj (default 18750) controls the cylinder curvature. */
|
|
28586
|
+
function canUpPath(w, h, t, adj) {
|
|
28587
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28588
|
+
const curvature = Math.max(0, Math.min(adjFactor, 4));
|
|
28589
|
+
const archH = h * (0.35 - t * 0.25) * curvature;
|
|
28590
|
+
if (archH < 1) {
|
|
28591
|
+
return `M 0,${h} L ${w},${h}`;
|
|
28592
|
+
}
|
|
28593
|
+
return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
|
|
28594
|
+
}
|
|
28595
|
+
/** Cylindrical text — downward.
|
|
28596
|
+
* adj (default 18750) controls the cylinder curvature. */
|
|
28597
|
+
function canDownPath(w, h, t, adj) {
|
|
28598
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28599
|
+
const curvature = Math.max(0, Math.min(adjFactor, 4));
|
|
28600
|
+
const archH = h * (0.1 + t * 0.25) * curvature;
|
|
28601
|
+
if (archH < 1) {
|
|
28602
|
+
return `M 0,0 L ${w},0`;
|
|
28603
|
+
}
|
|
28604
|
+
return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
|
|
28605
|
+
}
|
|
28606
|
+
/** Button shape — convex top / concave bottom.
|
|
28607
|
+
* adj (default 18750) controls the curve amount. */
|
|
28608
|
+
function buttonPath(w, h, t, adj) {
|
|
28609
|
+
const yBase = h * (0.1 + t * 0.8);
|
|
28610
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28611
|
+
const bulge = h * 0.15 * (1 - 2 * t) * Math.max(0, Math.min(adjFactor, 4));
|
|
28612
|
+
return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
|
|
28613
|
+
}
|
|
28614
|
+
/** Ring inside — concentric ellipses scaled inward.
|
|
28615
|
+
* adj (default 18750) controls ring thickness. */
|
|
28616
|
+
function ringInsidePath(w, h, t, adj) {
|
|
28617
|
+
const cx = w / 2;
|
|
28618
|
+
const cy = h / 2;
|
|
28619
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28620
|
+
const thickness = 0.35 * Math.max(0, Math.min(adjFactor, 4));
|
|
28621
|
+
const scale = 0.7 - t * thickness;
|
|
28622
|
+
const rx = Math.max(1, (w / 2) * scale);
|
|
28623
|
+
const ry = Math.max(1, (h / 2) * scale);
|
|
28624
|
+
return (`M ${cx},${cy - ry} ` +
|
|
28625
|
+
`A ${rx},${ry} 0 1,1 ${cx},${cy + ry} ` +
|
|
28626
|
+
`A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
|
|
28627
|
+
}
|
|
28628
|
+
/** Ring outside — concentric ellipses scaled outward.
|
|
28629
|
+
* adj (default 18750) controls ring thickness. */
|
|
28630
|
+
function ringOutsidePath(w, h, t, adj) {
|
|
28631
|
+
const cx = w / 2;
|
|
28632
|
+
const cy = h / 2;
|
|
28633
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28634
|
+
const thickness = 0.35 * Math.max(0, Math.min(adjFactor, 4));
|
|
28635
|
+
const scale = 1 - t * thickness;
|
|
28636
|
+
const rx = Math.max(1, (w / 2) * scale);
|
|
28637
|
+
const ry = Math.max(1, (h / 2) * scale);
|
|
28638
|
+
return (`M ${cx},${cy - ry} ` +
|
|
28639
|
+
`A ${rx},${ry} 0 1,1 ${cx},${cy + ry} ` +
|
|
28640
|
+
`A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
|
|
28641
|
+
}
|
|
28642
|
+
// ── Priority 3 path generators ─────────────────────────────────────────
|
|
28643
|
+
/** Triangle / trapezoid — top line narrow, bottom line full width.
|
|
28644
|
+
* adj (default 50000) controls the narrowness at the top. */
|
|
28645
|
+
function trianglePath(w, h, t, adj) {
|
|
28646
|
+
const adjRatio = adj !== undefined ? adj / 100000 : 0.5;
|
|
28647
|
+
const narrowW = w * (1 - Math.max(0, Math.min(adjRatio, 1))) * 0.3;
|
|
28648
|
+
const lineW = narrowW + t * (w - narrowW);
|
|
28649
|
+
const xStart = (w - lineW) / 2;
|
|
28650
|
+
const yBase = h * (0.1 + t * 0.8);
|
|
28651
|
+
return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
|
|
28652
|
+
}
|
|
28653
|
+
/** Inverted triangle — top line full width, bottom line narrow.
|
|
28654
|
+
* adj (default 50000) controls the narrowness at the bottom. */
|
|
28655
|
+
function triangleInvertedPath(w, h, t, adj) {
|
|
28656
|
+
const adjRatio = adj !== undefined ? adj / 100000 : 0.5;
|
|
28657
|
+
const narrowW = w * (1 - Math.max(0, Math.min(adjRatio, 1))) * 0.3;
|
|
28658
|
+
const lineW = w - t * (w - narrowW);
|
|
28659
|
+
const xStart = (w - lineW) / 2;
|
|
28660
|
+
const yBase = h * (0.1 + t * 0.8);
|
|
28661
|
+
return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
|
|
28662
|
+
}
|
|
28663
|
+
/** Stop / octagon — lines narrow at top and bottom, widest in centre.
|
|
28664
|
+
* adj (default 25000) controls the amount of corner inset. */
|
|
28665
|
+
function stopPath(w, h, t, adj) {
|
|
28666
|
+
const adjFactor = adj !== undefined ? adj / 25000 : 1;
|
|
28667
|
+
const insetScale = Math.max(0, Math.min(adjFactor, 4));
|
|
28668
|
+
const inset = w * 0.15 * (1 - (1 - 2 * Math.abs(t - 0.5)) ** 2) * insetScale;
|
|
28669
|
+
const yBase = h * (0.1 + t * 0.8);
|
|
28670
|
+
return `M ${inset},${yBase} L ${w - inset},${yBase}`;
|
|
28671
|
+
}
|
|
28672
|
+
/** Chevron — V-shape pointing down.
|
|
28673
|
+
* adj (default 25000) controls chevron point height. */
|
|
28674
|
+
function chevronPath(w, h, t, adj) {
|
|
28675
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28676
|
+
const adjFactor = adj !== undefined ? adj / 25000 : 1;
|
|
28677
|
+
const dip = h * 0.2 * (1 - t) * Math.max(0, Math.min(adjFactor, 4));
|
|
28678
|
+
return `M 0,${yBase} L ${w / 2},${yBase + dip} L ${w},${yBase}`;
|
|
28679
|
+
}
|
|
28680
|
+
/** Inverted chevron — V-shape pointing up.
|
|
28681
|
+
* adj (default 25000) controls chevron point height. */
|
|
28682
|
+
function chevronInvertedPath(w, h, t, adj) {
|
|
28683
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28684
|
+
const adjFactor = adj !== undefined ? adj / 25000 : 1;
|
|
28685
|
+
const rise = h * 0.2 * t * Math.max(0, Math.min(adjFactor, 4));
|
|
28686
|
+
return `M 0,${yBase} L ${w / 2},${yBase - rise} L ${w},${yBase}`;
|
|
28687
|
+
}
|
|
28688
|
+
/** Inflate bottom only.
|
|
28689
|
+
* adj (default 18750) controls bulge amount. */
|
|
28690
|
+
function inflateBottomPath(w, h, t, adj) {
|
|
28691
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28692
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28693
|
+
const bulge = t > 0.4 ? h * 0.25 * ((t - 0.4) / 0.6) * Math.max(0, Math.min(adjFactor, 4)) : 0;
|
|
28694
|
+
return `M 0,${yBase} Q ${w / 2},${yBase + bulge} ${w},${yBase}`;
|
|
28695
|
+
}
|
|
28696
|
+
/** Inflate top only.
|
|
28697
|
+
* adj (default 18750) controls bulge amount. */
|
|
28698
|
+
function inflateTopPath(w, h, t, adj) {
|
|
28699
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28700
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28701
|
+
const bulge = t < 0.6 ? h * 0.25 * ((0.6 - t) / 0.6) * Math.max(0, Math.min(adjFactor, 4)) : 0;
|
|
28702
|
+
return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
|
|
28703
|
+
}
|
|
28704
|
+
/** Deflate bottom only.
|
|
28705
|
+
* adj (default 18750) controls pinch amount. */
|
|
28706
|
+
function deflateBottomPath(w, h, t, adj) {
|
|
28707
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28708
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28709
|
+
const pinch = t > 0.4 ? h * 0.2 * ((t - 0.4) / 0.6) * Math.max(0, Math.min(adjFactor, 4)) : 0;
|
|
28710
|
+
return `M 0,${yBase} Q ${w / 2},${yBase - pinch} ${w},${yBase}`;
|
|
28711
|
+
}
|
|
28712
|
+
/** Deflate top only.
|
|
28713
|
+
* adj (default 18750) controls pinch amount. */
|
|
28714
|
+
function deflateTopPath(w, h, t, adj) {
|
|
28715
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28716
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28717
|
+
const pinch = t < 0.6 ? h * 0.2 * ((0.6 - t) / 0.6) * Math.max(0, Math.min(adjFactor, 4)) : 0;
|
|
28718
|
+
return `M 0,${yBase} Q ${w / 2},${yBase + pinch} ${w},${yBase}`;
|
|
28719
|
+
}
|
|
28720
|
+
// ── Priority 4 path generators ─────────────────────────────────────────
|
|
28721
|
+
/** Slant up — baseline rises from left to right.
|
|
28722
|
+
* adj (default 55000) controls the slant angle. */
|
|
28723
|
+
function slantUpPath(w, h, t, adj) {
|
|
28724
|
+
const adjFactor = adj !== undefined ? adj / 55000 : 1;
|
|
28725
|
+
const slant = 0.25 * Math.max(0, Math.min(adjFactor, 4));
|
|
28726
|
+
const yMid = h * (0.175 + t * 0.55);
|
|
28727
|
+
const yStart = yMid + (h * slant) / 2;
|
|
28728
|
+
const yEnd = yMid - (h * slant) / 2;
|
|
28729
|
+
return `M 0,${yStart} L ${w},${yEnd}`;
|
|
28730
|
+
}
|
|
28731
|
+
/** Slant down — baseline falls from left to right.
|
|
28732
|
+
* adj (default 55000) controls the slant angle. */
|
|
28733
|
+
function slantDownPath(w, h, t, adj) {
|
|
28734
|
+
const adjFactor = adj !== undefined ? adj / 55000 : 1;
|
|
28735
|
+
const slant = 0.25 * Math.max(0, Math.min(adjFactor, 4));
|
|
28736
|
+
const yMid = h * (0.175 + t * 0.55);
|
|
28737
|
+
const yStart = yMid - (h * slant) / 2;
|
|
28738
|
+
const yEnd = yMid + (h * slant) / 2;
|
|
28739
|
+
return `M 0,${yStart} L ${w},${yEnd}`;
|
|
28740
|
+
}
|
|
28741
|
+
/** Fade right — text narrows towards the right (trapezoid).
|
|
28742
|
+
* adj (default 50000) controls the fade/squeeze amount. */
|
|
28743
|
+
function fadeRightPath(w, h, t, adj) {
|
|
28744
|
+
const adjFactor = adj !== undefined ? adj / 50000 : 1;
|
|
28745
|
+
const squeezeScale = Math.max(0, Math.min(adjFactor, 4));
|
|
28746
|
+
const yLeft = h * (0.1 + t * 0.8);
|
|
28747
|
+
const squeeze = 0.35 * (1 - 2 * t) * squeezeScale;
|
|
28748
|
+
const yRight = h * (0.5 + squeeze * 0.4);
|
|
28749
|
+
return `M 0,${yLeft} L ${w},${yRight}`;
|
|
28750
|
+
}
|
|
28751
|
+
/** Fade left — text narrows towards the left (trapezoid).
|
|
28752
|
+
* adj (default 50000) controls the fade/squeeze amount. */
|
|
28753
|
+
function fadeLeftPath(w, h, t, adj) {
|
|
28754
|
+
const adjFactor = adj !== undefined ? adj / 50000 : 1;
|
|
28755
|
+
const squeezeScale = Math.max(0, Math.min(adjFactor, 4));
|
|
28756
|
+
const squeeze = 0.35 * (1 - 2 * t) * squeezeScale;
|
|
28757
|
+
const yLeft = h * (0.5 + squeeze * 0.4);
|
|
28758
|
+
const yRight = h * (0.1 + t * 0.8);
|
|
28759
|
+
return `M 0,${yLeft} L ${w},${yRight}`;
|
|
28760
|
+
}
|
|
28761
|
+
/** Fade up — text narrows towards the top.
|
|
28762
|
+
* adj (default 50000) controls the fade/taper amount. */
|
|
28763
|
+
function fadeUpPath(w, h, t, adj) {
|
|
28764
|
+
const adjFactor = adj !== undefined ? adj / 50000 : 1;
|
|
28765
|
+
const taperScale = Math.max(0, Math.min(adjFactor, 4));
|
|
28766
|
+
const narrowFraction = 1 - 0.7 * taperScale;
|
|
28767
|
+
const narrowW = w * Math.max(0, narrowFraction);
|
|
28768
|
+
const lineW = narrowW + t * (w - narrowW);
|
|
28769
|
+
const xStart = (w - lineW) / 2;
|
|
28770
|
+
const yBase = h * (0.1 + t * 0.8);
|
|
28771
|
+
return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
|
|
28772
|
+
}
|
|
28773
|
+
/** Fade down — text narrows towards the bottom.
|
|
28774
|
+
* adj (default 50000) controls the fade/taper amount. */
|
|
28775
|
+
function fadeDownPath(w, h, t, adj) {
|
|
28776
|
+
const adjFactor = adj !== undefined ? adj / 50000 : 1;
|
|
28777
|
+
const taperScale = Math.max(0, Math.min(adjFactor, 4));
|
|
28778
|
+
const narrowFraction = 1 - 0.7 * taperScale;
|
|
28779
|
+
const narrowW = w * Math.max(0, narrowFraction);
|
|
28780
|
+
const lineW = w - t * (w - narrowW);
|
|
28781
|
+
const xStart = (w - lineW) / 2;
|
|
28782
|
+
const yBase = h * (0.1 + t * 0.8);
|
|
28783
|
+
return `M ${xStart},${yBase} L ${xStart + lineW},${yBase}`;
|
|
28784
|
+
}
|
|
28785
|
+
/** Arch up pour — hollowed arch upward (like archUp but with inner hole).
|
|
28786
|
+
* adj (default 10800000) controls the arch height in 60000ths of a degree. */
|
|
28787
|
+
function archUpPourPath(w, h, t, adj) {
|
|
28788
|
+
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
28789
|
+
const maxArch = (0.7 * adjNorm) / 0.5;
|
|
28790
|
+
const archH = h * Math.max(0, maxArch - t * 0.5);
|
|
28791
|
+
if (archH < 1) {
|
|
28792
|
+
return `M 0,${h} L ${w},${h}`;
|
|
28793
|
+
}
|
|
28794
|
+
return `M 0,${h} A ${w / 2},${archH} 0 0,1 ${w},${h}`;
|
|
28795
|
+
}
|
|
28796
|
+
/** Arch down pour — hollowed arch downward.
|
|
28797
|
+
* adj (default 10800000) controls the arch depth in 60000ths of a degree. */
|
|
28798
|
+
function archDownPourPath(w, h, t, adj) {
|
|
28799
|
+
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
28800
|
+
const baseDepth = (0.2 * adjNorm) / 0.5;
|
|
28801
|
+
const archH = h * (baseDepth + t * 0.5);
|
|
28802
|
+
if (archH < 1) {
|
|
28803
|
+
return `M 0,0 L ${w},0`;
|
|
28804
|
+
}
|
|
28805
|
+
return `M 0,0 A ${w / 2},${archH} 0 0,0 ${w},0`;
|
|
28806
|
+
}
|
|
28807
|
+
/** Circle pour — concentric ellipses (like circle but with an inner gap).
|
|
28808
|
+
* adj (default 10800000) controls the arc span angle in 60000ths of a degree. */
|
|
28809
|
+
function circlePourPath(w, h, t, adj) {
|
|
28810
|
+
const cx = w / 2;
|
|
28811
|
+
const cy = h / 2;
|
|
28812
|
+
const adjNorm = adj !== undefined ? Math.max(0, Math.min(adj / 21600000, 1)) : 0.5;
|
|
28813
|
+
const baseScale = 0.35 + Number(adjNorm);
|
|
28814
|
+
const scale = Math.min(1, baseScale) - t * 0.45;
|
|
28815
|
+
const rx = Math.max(1, (w / 2) * scale);
|
|
28816
|
+
const ry = Math.max(1, (h / 2) * scale);
|
|
28817
|
+
return (`M ${cx},${cy - ry} ` +
|
|
28818
|
+
`A ${rx},${ry} 0 1,1 ${cx},${cy + ry} ` +
|
|
28819
|
+
`A ${rx},${ry} 0 1,1 ${cx},${cy - ry}`);
|
|
28820
|
+
}
|
|
28821
|
+
/** Button pour — convex top / concave bottom with larger margins.
|
|
28822
|
+
* adj (default 18750) controls the curve amount. */
|
|
28823
|
+
function buttonPourPath(w, h, t, adj) {
|
|
28824
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28825
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28826
|
+
const bulge = h * 0.12 * (1 - 2 * t) * Math.max(0, Math.min(adjFactor, 4));
|
|
28827
|
+
return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
|
|
28828
|
+
}
|
|
28829
|
+
/** Deflate-inflate — pinched in centre top/bottom, expanded at edges.
|
|
28830
|
+
* adj (default 18750) controls the oscillation amplitude. */
|
|
28831
|
+
function deflateInflatePath(w, h, t, adj) {
|
|
28832
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28833
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28834
|
+
const factor = Math.sin(t * Math.PI);
|
|
28835
|
+
const bulge = h * 0.2 * (factor - 0.5) * Math.max(0, Math.min(adjFactor, 4));
|
|
28836
|
+
return `M 0,${yBase} Q ${w / 2},${yBase - bulge} ${w},${yBase}`;
|
|
28837
|
+
}
|
|
28838
|
+
/** Deflate-inflate-deflate — triple oscillation.
|
|
28839
|
+
* adj (default 18750) controls the oscillation amplitude. */
|
|
28840
|
+
function deflateInflateDeflatePath(w, h, t, adj) {
|
|
28841
|
+
const yBase = h * (0.15 + t * 0.7);
|
|
28842
|
+
const adjFactor = adj !== undefined ? adj / 18750 : 1;
|
|
28843
|
+
const ampScale = Math.max(0, Math.min(adjFactor, 4));
|
|
28844
|
+
const factor = Math.sin(t * Math.PI * 2);
|
|
28845
|
+
const bulge = h * 0.15 * factor * ampScale;
|
|
28846
|
+
const q1 = w / 3;
|
|
28847
|
+
const q2 = (2 * w) / 3;
|
|
28848
|
+
return (`M 0,${yBase} ` +
|
|
28849
|
+
`Q ${q1},${yBase - bulge} ${w / 2},${yBase} ` +
|
|
28850
|
+
`Q ${q2},${yBase + bulge} ${w},${yBase}`);
|
|
28851
|
+
}
|
|
28852
|
+
// ── Generator look-up table ────────────────────────────────────────────
|
|
28853
|
+
const WARP_PATH_GENERATORS = {
|
|
28854
|
+
textArchUp: archUpPath,
|
|
28855
|
+
textArchDown: archDownPath,
|
|
28856
|
+
textCircle: circlePath,
|
|
28857
|
+
textWave1: wave1Path,
|
|
28858
|
+
textInflate: inflatePath,
|
|
28859
|
+
textDeflate: deflatePath,
|
|
28860
|
+
textCurveUp: curveUpPath,
|
|
28861
|
+
textCurveDown: curveDownPath,
|
|
28862
|
+
textWave2: wave2Path,
|
|
28863
|
+
textWave4: wave4Path,
|
|
28864
|
+
textDoubleWave1: doubleWave1Path,
|
|
28865
|
+
textCanUp: canUpPath,
|
|
28866
|
+
textCanDown: canDownPath,
|
|
28867
|
+
textButton: buttonPath,
|
|
28868
|
+
textRingInside: ringInsidePath,
|
|
28869
|
+
textRingOutside: ringOutsidePath,
|
|
28870
|
+
textCascadeUp: cascadeUpPath,
|
|
28871
|
+
textCascadeDown: cascadeDownPath,
|
|
28872
|
+
textTriangle: trianglePath,
|
|
28873
|
+
textTriangleInverted: triangleInvertedPath,
|
|
28874
|
+
textStop: stopPath,
|
|
28875
|
+
textChevron: chevronPath,
|
|
28876
|
+
textChevronInverted: chevronInvertedPath,
|
|
28877
|
+
textInflateBottom: inflateBottomPath,
|
|
28878
|
+
textInflateTop: inflateTopPath,
|
|
28879
|
+
textDeflateBottom: deflateBottomPath,
|
|
28880
|
+
textDeflateTop: deflateTopPath,
|
|
28881
|
+
textSlantUp: slantUpPath,
|
|
28882
|
+
textSlantDown: slantDownPath,
|
|
28883
|
+
textFadeRight: fadeRightPath,
|
|
28884
|
+
textFadeLeft: fadeLeftPath,
|
|
28885
|
+
textFadeUp: fadeUpPath,
|
|
28886
|
+
textFadeDown: fadeDownPath,
|
|
28887
|
+
textArchUpPour: archUpPourPath,
|
|
28888
|
+
textArchDownPour: archDownPourPath,
|
|
28889
|
+
textCirclePour: circlePourPath,
|
|
28890
|
+
textButtonPour: buttonPourPath,
|
|
28891
|
+
textDeflateInflate: deflateInflatePath,
|
|
28892
|
+
textDeflateInflateDeflate: deflateInflateDeflatePath,
|
|
28893
|
+
};
|
|
28894
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
28895
|
+
/** Returns `true` when the preset should use SVG `<textPath>` rendering. */
|
|
28896
|
+
function shouldUseSvgWarp(preset) {
|
|
28897
|
+
if (!preset || preset === 'textNoShape' || preset === 'textPlain') {
|
|
28898
|
+
return false;
|
|
28899
|
+
}
|
|
28900
|
+
return SVG_WARP_PRESETS.has(preset);
|
|
28901
|
+
}
|
|
28902
|
+
/** Generate an SVG path `d` attribute for a warp preset at a given line position.
|
|
28903
|
+
* Optional adj/adj2 are raw OOXML adjustment values (1/60000th units). */
|
|
28904
|
+
function getWarpPath(preset, width, height, lineIndex, lineCount, adj, adj2) {
|
|
28905
|
+
const t = lineCount <= 1 ? 0.5 : lineIndex / (lineCount - 1);
|
|
28906
|
+
const generator = WARP_PATH_GENERATORS[preset];
|
|
28907
|
+
if (generator) {
|
|
28908
|
+
return generator(width, height, t, adj, adj2);
|
|
28909
|
+
}
|
|
28910
|
+
const y = height * (0.2 + t * 0.6);
|
|
28911
|
+
return `M 0,${y} L ${width},${y}`;
|
|
28912
|
+
}
|
|
28913
|
+
|
|
28914
|
+
const NONE_PRESETS = new Set(['textNoShape', 'textPlain']);
|
|
28915
|
+
/** Presets that render best with SVG textPath along curved/circular paths. */
|
|
28916
|
+
const PATH_PRESETS = new Set([
|
|
28917
|
+
'textArchUp',
|
|
28918
|
+
'textArchDown',
|
|
28919
|
+
'textCircle',
|
|
28920
|
+
'textWave1',
|
|
28921
|
+
'textWave2',
|
|
28922
|
+
'textWave4',
|
|
28923
|
+
'textDoubleWave1',
|
|
28924
|
+
'textCurveUp',
|
|
28925
|
+
'textCurveDown',
|
|
28926
|
+
'textArchUpPour',
|
|
28927
|
+
'textArchDownPour',
|
|
28928
|
+
'textCirclePour',
|
|
28929
|
+
'textButton',
|
|
28930
|
+
'textButtonPour',
|
|
28931
|
+
'textRingInside',
|
|
28932
|
+
'textRingOutside',
|
|
28933
|
+
'textTriangle',
|
|
28934
|
+
'textTriangleInverted',
|
|
28935
|
+
'textChevron',
|
|
28936
|
+
'textChevronInverted',
|
|
28937
|
+
'textStop',
|
|
28938
|
+
]);
|
|
28939
|
+
/**
|
|
28940
|
+
* Envelope presets stretch text non-uniformly (wider/narrower per line).
|
|
28941
|
+
* CSS transforms approximate the visual effect per text block.
|
|
28942
|
+
*/
|
|
28943
|
+
const ENVELOPE_PRESETS = new Set([
|
|
28944
|
+
'textInflate',
|
|
28945
|
+
'textDeflate',
|
|
28946
|
+
'textInflateBottom',
|
|
28947
|
+
'textInflateTop',
|
|
28948
|
+
'textDeflateBottom',
|
|
28949
|
+
'textDeflateTop',
|
|
28950
|
+
'textDeflateInflate',
|
|
28951
|
+
'textDeflateInflateDeflate',
|
|
28952
|
+
'textCanUp',
|
|
28953
|
+
'textCanDown',
|
|
28954
|
+
]);
|
|
28955
|
+
/** Simple presets that work with basic 2D CSS transforms (skew, perspective). */
|
|
28956
|
+
const SIMPLE_PRESETS = new Set([
|
|
28957
|
+
'textSlantUp',
|
|
28958
|
+
'textSlantDown',
|
|
28959
|
+
'textFadeRight',
|
|
28960
|
+
'textFadeLeft',
|
|
28961
|
+
'textFadeUp',
|
|
28962
|
+
'textFadeDown',
|
|
28963
|
+
'textCascadeUp',
|
|
28964
|
+
'textCascadeDown',
|
|
28965
|
+
]);
|
|
28966
|
+
/** All known classified presets (excludes `none`-family). */
|
|
28967
|
+
const ALL_CLASSIFIED_PRESETS = new Set([
|
|
28968
|
+
...NONE_PRESETS,
|
|
28969
|
+
...PATH_PRESETS,
|
|
28970
|
+
...ENVELOPE_PRESETS,
|
|
28971
|
+
...SIMPLE_PRESETS,
|
|
28972
|
+
]);
|
|
28973
|
+
/**
|
|
28974
|
+
* Classify a warp preset into a rendering strategy category.
|
|
28975
|
+
*
|
|
28976
|
+
* Returns `'none'` for unknown or empty presets so callers can safely
|
|
28977
|
+
* skip rendering without an explicit allowlist check.
|
|
28978
|
+
*/
|
|
28979
|
+
function getWarpCategory(preset) {
|
|
28980
|
+
if (!preset || NONE_PRESETS.has(preset)) {
|
|
28981
|
+
return 'none';
|
|
28982
|
+
}
|
|
28983
|
+
if (PATH_PRESETS.has(preset)) {
|
|
28984
|
+
return 'path';
|
|
28985
|
+
}
|
|
28986
|
+
if (ENVELOPE_PRESETS.has(preset)) {
|
|
28987
|
+
return 'envelope';
|
|
28988
|
+
}
|
|
28989
|
+
if (SIMPLE_PRESETS.has(preset)) {
|
|
28990
|
+
return 'simple';
|
|
28991
|
+
}
|
|
28992
|
+
return 'none';
|
|
28993
|
+
}
|
|
28994
|
+
/**
|
|
28995
|
+
* Split an element's `textSegments` into paragraphs.
|
|
28996
|
+
* Paragraph-break segments are used as delimiters and excluded from output.
|
|
28997
|
+
* Falls back to a single synthetic paragraph when only `element.text` is set.
|
|
28998
|
+
*/
|
|
28999
|
+
function groupIntoParagraphs(el) {
|
|
29000
|
+
const segs = el.textSegments;
|
|
29001
|
+
if (!segs || segs.length === 0) {
|
|
29002
|
+
if (el.text) {
|
|
29003
|
+
return [{ segments: [{ text: el.text, style: {} }] }];
|
|
29004
|
+
}
|
|
29005
|
+
return [];
|
|
29006
|
+
}
|
|
29007
|
+
const paragraphs = [];
|
|
29008
|
+
let current = [];
|
|
29009
|
+
for (const seg of segs) {
|
|
29010
|
+
if (seg.isParagraphBreak) {
|
|
29011
|
+
if (current.length > 0) {
|
|
29012
|
+
paragraphs.push({ segments: current });
|
|
29013
|
+
}
|
|
29014
|
+
current = [];
|
|
29015
|
+
}
|
|
29016
|
+
else {
|
|
29017
|
+
current.push(seg);
|
|
29018
|
+
}
|
|
29019
|
+
}
|
|
29020
|
+
if (current.length > 0) {
|
|
29021
|
+
paragraphs.push({ segments: current });
|
|
29022
|
+
}
|
|
29023
|
+
return paragraphs;
|
|
29024
|
+
}
|
|
29025
|
+
// ── CSS transform generators ───────────────────────────────────────────
|
|
29026
|
+
// (mirrors text-warp-classifier.ts getEnvelopeCssTransform / getSimpleCssTransform)
|
|
29027
|
+
/** Default OOXML adjustment values for envelope presets (raw 1/60000th units). */
|
|
29028
|
+
const ENVELOPE_DEFAULTS = {
|
|
29029
|
+
textInflate: 18750,
|
|
29030
|
+
textDeflate: 18750,
|
|
29031
|
+
textInflateBottom: 18750,
|
|
29032
|
+
textInflateTop: 18750,
|
|
29033
|
+
textDeflateBottom: 18750,
|
|
29034
|
+
textDeflateTop: 18750,
|
|
29035
|
+
textDeflateInflate: 18750,
|
|
29036
|
+
textDeflateInflateDeflate: 18750,
|
|
29037
|
+
textCanUp: 18750,
|
|
29038
|
+
textCanDown: 18750,
|
|
29039
|
+
};
|
|
29040
|
+
/** Resolve CSS transform + origin for envelope-family presets. */
|
|
29041
|
+
function envelopeCss(preset, adj1) {
|
|
29042
|
+
const defaultAdj = ENVELOPE_DEFAULTS[preset] ?? 18750;
|
|
29043
|
+
const a1 = adj1 ?? defaultAdj;
|
|
29044
|
+
const intensity = Math.max(0, Math.min(a1 / 18750, 4));
|
|
29045
|
+
switch (preset) {
|
|
29046
|
+
case 'textInflate':
|
|
29047
|
+
return {
|
|
29048
|
+
transform: `scaleY(${1 + 0.15 * intensity}) scaleX(${1 + 0.05 * intensity})`,
|
|
29049
|
+
transformOrigin: 'center center',
|
|
29050
|
+
};
|
|
29051
|
+
case 'textInflateBottom':
|
|
29052
|
+
return {
|
|
29053
|
+
transform: `perspective(${600 - 100 * intensity}px) rotateX(${-8 * intensity}deg)`,
|
|
29054
|
+
transformOrigin: 'center bottom',
|
|
29055
|
+
};
|
|
29056
|
+
case 'textInflateTop':
|
|
29057
|
+
return {
|
|
29058
|
+
transform: `perspective(${600 - 100 * intensity}px) rotateX(${8 * intensity}deg)`,
|
|
29059
|
+
transformOrigin: 'center top',
|
|
29060
|
+
};
|
|
29061
|
+
case 'textDeflate':
|
|
29062
|
+
return {
|
|
29063
|
+
transform: `scaleY(${1 - 0.12 * intensity}) scaleX(${1 - 0.05 * intensity})`,
|
|
29064
|
+
transformOrigin: 'center center',
|
|
29065
|
+
};
|
|
29066
|
+
case 'textDeflateBottom':
|
|
29067
|
+
return {
|
|
29068
|
+
transform: `perspective(${600 - 100 * intensity}px) rotateX(${6 * intensity}deg)`,
|
|
29069
|
+
transformOrigin: 'center bottom',
|
|
29070
|
+
};
|
|
29071
|
+
case 'textDeflateTop':
|
|
29072
|
+
return {
|
|
29073
|
+
transform: `perspective(${600 - 100 * intensity}px) rotateX(${-6 * intensity}deg)`,
|
|
29074
|
+
transformOrigin: 'center top',
|
|
29075
|
+
};
|
|
29076
|
+
case 'textDeflateInflate':
|
|
29077
|
+
return {
|
|
29078
|
+
transform: `scaleY(${1 - 0.08 * intensity}) scaleX(${1 + 0.04 * intensity})`,
|
|
29079
|
+
transformOrigin: 'center center',
|
|
29080
|
+
};
|
|
29081
|
+
case 'textDeflateInflateDeflate':
|
|
29082
|
+
return {
|
|
29083
|
+
transform: `scaleY(${1 - 0.15 * intensity}) scaleX(${1 + 0.06 * intensity})`,
|
|
29084
|
+
transformOrigin: 'center center',
|
|
29085
|
+
};
|
|
29086
|
+
case 'textCanUp':
|
|
29087
|
+
return {
|
|
29088
|
+
transform: `perspective(${500 - 80 * intensity}px) rotateX(${-6 * intensity}deg)`,
|
|
29089
|
+
transformOrigin: 'center center',
|
|
29090
|
+
};
|
|
29091
|
+
case 'textCanDown':
|
|
29092
|
+
return {
|
|
29093
|
+
transform: `perspective(${500 - 80 * intensity}px) rotateX(${6 * intensity}deg)`,
|
|
29094
|
+
transformOrigin: 'center center',
|
|
29095
|
+
};
|
|
29096
|
+
default:
|
|
29097
|
+
return undefined;
|
|
29098
|
+
}
|
|
29099
|
+
}
|
|
29100
|
+
/** Default OOXML adjustment values for simple presets (raw 1/60000th units). */
|
|
29101
|
+
const SIMPLE_DEFAULTS = {
|
|
29102
|
+
textSlantUp: 55000,
|
|
29103
|
+
textSlantDown: 55000,
|
|
29104
|
+
textFadeRight: 50000,
|
|
29105
|
+
textFadeLeft: 50000,
|
|
29106
|
+
textFadeUp: 50000,
|
|
29107
|
+
textFadeDown: 50000,
|
|
29108
|
+
textCascadeUp: 44444,
|
|
29109
|
+
textCascadeDown: 44444,
|
|
29110
|
+
};
|
|
29111
|
+
/** Resolve CSS transform + origin for simple-family presets. */
|
|
29112
|
+
function simpleCss(preset, adj1) {
|
|
29113
|
+
const defaultAdj = SIMPLE_DEFAULTS[preset] ?? 50000;
|
|
29114
|
+
const a1 = adj1 ?? defaultAdj;
|
|
29115
|
+
switch (preset) {
|
|
29116
|
+
case 'textSlantUp': {
|
|
29117
|
+
const skew = -4 * (a1 / 55000);
|
|
29118
|
+
return {
|
|
29119
|
+
transform: `perspective(500px) rotateY(${8 * (a1 / 55000)}deg) skewY(${skew}deg)`,
|
|
29120
|
+
transformOrigin: 'left center',
|
|
29121
|
+
};
|
|
29122
|
+
}
|
|
29123
|
+
case 'textSlantDown': {
|
|
29124
|
+
const skew = 4 * (a1 / 55000);
|
|
29125
|
+
return {
|
|
29126
|
+
transform: `perspective(500px) rotateY(${-8 * (a1 / 55000)}deg) skewY(${skew}deg)`,
|
|
29127
|
+
transformOrigin: 'right center',
|
|
29128
|
+
};
|
|
29129
|
+
}
|
|
29130
|
+
case 'textFadeRight': {
|
|
29131
|
+
const angle = 10 * (a1 / 50000);
|
|
29132
|
+
return {
|
|
29133
|
+
transform: `perspective(400px) rotateY(${-angle}deg)`,
|
|
29134
|
+
transformOrigin: 'left center',
|
|
29135
|
+
};
|
|
29136
|
+
}
|
|
29137
|
+
case 'textFadeLeft': {
|
|
29138
|
+
const angle = 10 * (a1 / 50000);
|
|
29139
|
+
return {
|
|
29140
|
+
transform: `perspective(400px) rotateY(${angle}deg)`,
|
|
29141
|
+
transformOrigin: 'right center',
|
|
29142
|
+
};
|
|
29143
|
+
}
|
|
29144
|
+
case 'textFadeUp': {
|
|
29145
|
+
const angle = 10 * (a1 / 50000);
|
|
29146
|
+
return {
|
|
29147
|
+
transform: `perspective(400px) rotateX(${-angle}deg)`,
|
|
29148
|
+
transformOrigin: 'center bottom',
|
|
29149
|
+
};
|
|
29150
|
+
}
|
|
29151
|
+
case 'textFadeDown': {
|
|
29152
|
+
const angle = 10 * (a1 / 50000);
|
|
29153
|
+
return {
|
|
29154
|
+
transform: `perspective(400px) rotateX(${angle}deg)`,
|
|
29155
|
+
transformOrigin: 'center top',
|
|
29156
|
+
};
|
|
29157
|
+
}
|
|
29158
|
+
case 'textCascadeUp': {
|
|
29159
|
+
const skew = -8 * (a1 / 44444);
|
|
29160
|
+
return {
|
|
29161
|
+
transform: `skewY(${skew}deg)`,
|
|
29162
|
+
transformOrigin: 'left top',
|
|
29163
|
+
};
|
|
29164
|
+
}
|
|
29165
|
+
case 'textCascadeDown': {
|
|
29166
|
+
const skew = 8 * (a1 / 44444);
|
|
29167
|
+
return {
|
|
29168
|
+
transform: `skewY(${skew}deg)`,
|
|
29169
|
+
transformOrigin: 'left top',
|
|
29170
|
+
};
|
|
29171
|
+
}
|
|
29172
|
+
default:
|
|
29173
|
+
return undefined;
|
|
29174
|
+
}
|
|
29175
|
+
}
|
|
29176
|
+
// ── SVG alignment helpers ──────────────────────────────────────────────
|
|
29177
|
+
/** Map paragraph alignment to SVG textPath properties. */
|
|
29178
|
+
function resolveAlignment(align) {
|
|
29179
|
+
switch (align) {
|
|
29180
|
+
case 'center':
|
|
29181
|
+
return { startOffset: '50%', textAnchor: 'middle' };
|
|
29182
|
+
case 'right':
|
|
29183
|
+
return { startOffset: '100%', textAnchor: 'end' };
|
|
29184
|
+
default:
|
|
29185
|
+
return { startOffset: '0%', textAnchor: 'start' };
|
|
29186
|
+
}
|
|
29187
|
+
}
|
|
29188
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
29189
|
+
const DEFAULT_FONT_SIZE = 18;
|
|
29190
|
+
const DEFAULT_FONT_FAMILY = 'Calibri, sans-serif';
|
|
29191
|
+
const DEFAULT_COLOR = '#000000';
|
|
29192
|
+
/**
|
|
29193
|
+
* Resolve a `PptxElement`'s text warp preset into a `TextWarpDef` descriptor,
|
|
29194
|
+
* or `undefined` when the element carries no warp (or the preset is `textNoShape` /
|
|
29195
|
+
* `textPlain` / unknown).
|
|
29196
|
+
*
|
|
29197
|
+
* @param element Any `PptxElement`. Elements without text properties always
|
|
29198
|
+
* return `undefined`.
|
|
29199
|
+
* @returns A `TextWarpDef` with `strategy: 'path'` for SVG textPath warps, or
|
|
29200
|
+
* `strategy: 'css'` for CSS-transform approximations.
|
|
29201
|
+
*/
|
|
29202
|
+
function getTextWarp(element) {
|
|
29203
|
+
if (!hasTextProperties(element)) {
|
|
29204
|
+
return undefined;
|
|
29205
|
+
}
|
|
29206
|
+
const ts = element.textStyle;
|
|
29207
|
+
const preset = ts?.textWarpPreset;
|
|
29208
|
+
if (!preset || preset === 'textNoShape' || preset === 'textPlain') {
|
|
29209
|
+
return undefined;
|
|
29210
|
+
}
|
|
29211
|
+
const adj1 = ts?.textWarpAdj;
|
|
29212
|
+
const adj2 = ts?.textWarpAdj2;
|
|
29213
|
+
// ── Strategy: SVG path ──────────────────────────────────────────────
|
|
29214
|
+
if (shouldUseSvgWarp(preset)) {
|
|
29215
|
+
const paragraphs = groupIntoParagraphs(element);
|
|
29216
|
+
if (paragraphs.length === 0) {
|
|
29217
|
+
return undefined;
|
|
29218
|
+
}
|
|
29219
|
+
const lineCount = paragraphs.length;
|
|
29220
|
+
const width = element.width;
|
|
29221
|
+
const height = element.height;
|
|
29222
|
+
const pathIdPrefix = `ng-warp-${element.id}`;
|
|
29223
|
+
const { startOffset, textAnchor } = resolveAlignment(ts?.align);
|
|
29224
|
+
const pathLines = paragraphs.map((para, i) => ({
|
|
29225
|
+
pathId: `${pathIdPrefix}-${i}`,
|
|
29226
|
+
d: getWarpPath(preset, width, height, i, lineCount, adj1, adj2),
|
|
29227
|
+
segments: para.segments,
|
|
29228
|
+
}));
|
|
29229
|
+
return {
|
|
29230
|
+
strategy: 'path',
|
|
29231
|
+
preset,
|
|
29232
|
+
pathLines,
|
|
29233
|
+
width,
|
|
29234
|
+
height,
|
|
29235
|
+
textAnchor,
|
|
29236
|
+
startOffset,
|
|
29237
|
+
baseFontSize: (ts?.fontSize ?? DEFAULT_FONT_SIZE),
|
|
29238
|
+
baseFontFamily: ts?.fontFamily ?? DEFAULT_FONT_FAMILY,
|
|
29239
|
+
baseColor: ts?.color ?? DEFAULT_COLOR,
|
|
29240
|
+
};
|
|
29241
|
+
}
|
|
29242
|
+
// ── Strategy: CSS transform ─────────────────────────────────────────
|
|
29243
|
+
const category = getWarpCategory(preset);
|
|
29244
|
+
let cssDef;
|
|
29245
|
+
if (category === 'envelope') {
|
|
29246
|
+
cssDef = envelopeCss(preset, adj1);
|
|
29247
|
+
}
|
|
29248
|
+
else if (category === 'simple') {
|
|
29249
|
+
cssDef = simpleCss(preset, adj1);
|
|
29250
|
+
}
|
|
29251
|
+
if (!cssDef) {
|
|
29252
|
+
return undefined;
|
|
29253
|
+
}
|
|
29254
|
+
return {
|
|
29255
|
+
strategy: 'css',
|
|
29256
|
+
preset,
|
|
29257
|
+
cssTransform: cssDef.transform,
|
|
29258
|
+
cssTransformOrigin: cssDef.transformOrigin,
|
|
29259
|
+
};
|
|
29260
|
+
}
|
|
29261
|
+
|
|
27656
29262
|
/**
|
|
27657
29263
|
* Build the complete zoom view-model for a given element.
|
|
27658
29264
|
* Returns sensible defaults for non-zoom elements (all derived strings are
|
|
@@ -27856,6 +29462,25 @@ class ElementRendererComponent {
|
|
|
27856
29462
|
...(ngDevMode ? [{ debugName: "textStyle" }] : /* istanbul ignore next */ []));
|
|
27857
29463
|
imageSrc = computed(() => getImageSrc(this.element(), this.mediaDataUrls()), /* @ts-ignore */
|
|
27858
29464
|
...(ngDevMode ? [{ debugName: "imageSrc" }] : /* istanbul ignore next */ []));
|
|
29465
|
+
/** Text-warp (WordArt) descriptor for the element, if any. */
|
|
29466
|
+
textWarp = computed(() => getTextWarp(this.element()), /* @ts-ignore */
|
|
29467
|
+
...(ngDevMode ? [{ debugName: "textWarp" }] : /* istanbul ignore next */ []));
|
|
29468
|
+
/** Only the SVG-textPath warp variant (for the `<svg>` overlay branch). */
|
|
29469
|
+
pathWarp = computed(() => {
|
|
29470
|
+
const w = this.textWarp();
|
|
29471
|
+
return w?.strategy === 'path' ? w : undefined;
|
|
29472
|
+
}, /* @ts-ignore */
|
|
29473
|
+
...(ngDevMode ? [{ debugName: "pathWarp" }] : /* istanbul ignore next */ []));
|
|
29474
|
+
/** Text block style, folding in a CSS-transform warp when present. */
|
|
29475
|
+
warpedTextStyle = computed(() => {
|
|
29476
|
+
const base = this.textStyle();
|
|
29477
|
+
const w = this.textWarp();
|
|
29478
|
+
if (w?.strategy === 'css') {
|
|
29479
|
+
return { ...base, transform: w.cssTransform, 'transform-origin': w.cssTransformOrigin };
|
|
29480
|
+
}
|
|
29481
|
+
return base;
|
|
29482
|
+
}, /* @ts-ignore */
|
|
29483
|
+
...(ngDevMode ? [{ debugName: "warpedTextStyle" }] : /* istanbul ignore next */ []));
|
|
27859
29484
|
children = computed(() => {
|
|
27860
29485
|
const el = this.element();
|
|
27861
29486
|
return el.type === 'group' ? (el.children ?? []) : [];
|
|
@@ -28077,8 +29702,45 @@ class ElementRendererComponent {
|
|
|
28077
29702
|
[ngStyle]="shapeContainerStyle()"
|
|
28078
29703
|
[attr.data-element-id]="element().id"
|
|
28079
29704
|
>
|
|
28080
|
-
@if (
|
|
28081
|
-
<
|
|
29705
|
+
@if (pathWarp(); as warp) {
|
|
29706
|
+
<svg
|
|
29707
|
+
[attr.width]="warp.width"
|
|
29708
|
+
[attr.height]="warp.height"
|
|
29709
|
+
[attr.viewBox]="'0 0 ' + warp.width + ' ' + warp.height"
|
|
29710
|
+
style="position: absolute; inset: 0; overflow: visible; pointer-events: none"
|
|
29711
|
+
aria-hidden="true"
|
|
29712
|
+
>
|
|
29713
|
+
<defs>
|
|
29714
|
+
@for (line of warp.pathLines; track line.pathId) {
|
|
29715
|
+
<path [attr.id]="line.pathId" [attr.d]="line.d" fill="none" />
|
|
29716
|
+
}
|
|
29717
|
+
</defs>
|
|
29718
|
+
@for (line of warp.pathLines; track line.pathId) {
|
|
29719
|
+
<text
|
|
29720
|
+
[attr.font-size]="warp.baseFontSize"
|
|
29721
|
+
[attr.font-family]="warp.baseFontFamily"
|
|
29722
|
+
[attr.fill]="warp.baseColor"
|
|
29723
|
+
>
|
|
29724
|
+
<textPath
|
|
29725
|
+
[attr.href]="'#' + line.pathId"
|
|
29726
|
+
[attr.startOffset]="warp.startOffset"
|
|
29727
|
+
[attr.text-anchor]="warp.textAnchor"
|
|
29728
|
+
>
|
|
29729
|
+
@for (seg of line.segments; track $index) {
|
|
29730
|
+
<tspan
|
|
29731
|
+
[attr.fill]="seg.style?.color ?? warp.baseColor"
|
|
29732
|
+
[attr.font-weight]="seg.style?.bold ? 700 : 400"
|
|
29733
|
+
[attr.font-style]="seg.style?.italic ? 'italic' : 'normal'"
|
|
29734
|
+
>
|
|
29735
|
+
{{ seg.text }}
|
|
29736
|
+
</tspan>
|
|
29737
|
+
}
|
|
29738
|
+
</textPath>
|
|
29739
|
+
</text>
|
|
29740
|
+
}
|
|
29741
|
+
</svg>
|
|
29742
|
+
} @else if (hasText()) {
|
|
29743
|
+
<div class="pptx-ng-text" [ngStyle]="warpedTextStyle()">
|
|
28082
29744
|
@for (para of paragraphs(); track $index) {
|
|
28083
29745
|
<p class="pptx-ng-para" [style.padding-left.px]="para.indentPx">
|
|
28084
29746
|
@if (para.bulletMarker) {
|
|
@@ -28294,8 +29956,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
28294
29956
|
[ngStyle]="shapeContainerStyle()"
|
|
28295
29957
|
[attr.data-element-id]="element().id"
|
|
28296
29958
|
>
|
|
28297
|
-
@if (
|
|
28298
|
-
<
|
|
29959
|
+
@if (pathWarp(); as warp) {
|
|
29960
|
+
<svg
|
|
29961
|
+
[attr.width]="warp.width"
|
|
29962
|
+
[attr.height]="warp.height"
|
|
29963
|
+
[attr.viewBox]="'0 0 ' + warp.width + ' ' + warp.height"
|
|
29964
|
+
style="position: absolute; inset: 0; overflow: visible; pointer-events: none"
|
|
29965
|
+
aria-hidden="true"
|
|
29966
|
+
>
|
|
29967
|
+
<defs>
|
|
29968
|
+
@for (line of warp.pathLines; track line.pathId) {
|
|
29969
|
+
<path [attr.id]="line.pathId" [attr.d]="line.d" fill="none" />
|
|
29970
|
+
}
|
|
29971
|
+
</defs>
|
|
29972
|
+
@for (line of warp.pathLines; track line.pathId) {
|
|
29973
|
+
<text
|
|
29974
|
+
[attr.font-size]="warp.baseFontSize"
|
|
29975
|
+
[attr.font-family]="warp.baseFontFamily"
|
|
29976
|
+
[attr.fill]="warp.baseColor"
|
|
29977
|
+
>
|
|
29978
|
+
<textPath
|
|
29979
|
+
[attr.href]="'#' + line.pathId"
|
|
29980
|
+
[attr.startOffset]="warp.startOffset"
|
|
29981
|
+
[attr.text-anchor]="warp.textAnchor"
|
|
29982
|
+
>
|
|
29983
|
+
@for (seg of line.segments; track $index) {
|
|
29984
|
+
<tspan
|
|
29985
|
+
[attr.fill]="seg.style?.color ?? warp.baseColor"
|
|
29986
|
+
[attr.font-weight]="seg.style?.bold ? 700 : 400"
|
|
29987
|
+
[attr.font-style]="seg.style?.italic ? 'italic' : 'normal'"
|
|
29988
|
+
>
|
|
29989
|
+
{{ seg.text }}
|
|
29990
|
+
</tspan>
|
|
29991
|
+
}
|
|
29992
|
+
</textPath>
|
|
29993
|
+
</text>
|
|
29994
|
+
}
|
|
29995
|
+
</svg>
|
|
29996
|
+
} @else if (hasText()) {
|
|
29997
|
+
<div class="pptx-ng-text" [ngStyle]="warpedTextStyle()">
|
|
28299
29998
|
@for (para of paragraphs(); track $index) {
|
|
28300
29999
|
<p class="pptx-ng-para" [style.padding-left.px]="para.indentPx">
|
|
28301
30000
|
@if (para.bulletMarker) {
|
|
@@ -29777,6 +31476,779 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
29777
31476
|
type: Injectable
|
|
29778
31477
|
}], ctorParameters: () => [] });
|
|
29779
31478
|
|
|
31479
|
+
/**
|
|
31480
|
+
* presentation-annotations-helpers.ts — Pure geometry helpers for presentation
|
|
31481
|
+
* ink annotations (pen, highlighter, eraser, laser).
|
|
31482
|
+
*
|
|
31483
|
+
* Ported from React:
|
|
31484
|
+
* packages/react/src/viewer/components/PresentationAnnotationOverlay.tsx
|
|
31485
|
+
* packages/react/src/viewer/hooks/usePresentationAnnotations.ts
|
|
31486
|
+
*
|
|
31487
|
+
* No Angular dependencies — all functions are pure so they can be unit-tested
|
|
31488
|
+
* without TestBed.
|
|
31489
|
+
*/
|
|
31490
|
+
// ---------------------------------------------------------------------------
|
|
31491
|
+
// Constants
|
|
31492
|
+
// ---------------------------------------------------------------------------
|
|
31493
|
+
const PEN_WIDTH = 2.5;
|
|
31494
|
+
const HIGHLIGHTER_WIDTH = 14;
|
|
31495
|
+
const HIGHLIGHTER_OPACITY = 0.4;
|
|
31496
|
+
const ERASER_RADIUS = 16;
|
|
31497
|
+
// ---------------------------------------------------------------------------
|
|
31498
|
+
// Stroke id generation
|
|
31499
|
+
// ---------------------------------------------------------------------------
|
|
31500
|
+
let strokeIdCounter = 0;
|
|
31501
|
+
/**
|
|
31502
|
+
* Generate a monotonically-increasing stroke id.
|
|
31503
|
+
* Module-level counter is fine: ids only need to be unique within a session.
|
|
31504
|
+
*/
|
|
31505
|
+
function nextStrokeId() {
|
|
31506
|
+
strokeIdCounter += 1;
|
|
31507
|
+
return `stroke-${strokeIdCounter}`;
|
|
31508
|
+
}
|
|
31509
|
+
/**
|
|
31510
|
+
* Reset the stroke-id counter. Exposed for tests only — never call from
|
|
31511
|
+
* production code.
|
|
31512
|
+
*/
|
|
31513
|
+
function resetStrokeIdCounter() {
|
|
31514
|
+
strokeIdCounter = 0;
|
|
31515
|
+
}
|
|
31516
|
+
// ---------------------------------------------------------------------------
|
|
31517
|
+
// SVG path generation
|
|
31518
|
+
// ---------------------------------------------------------------------------
|
|
31519
|
+
/**
|
|
31520
|
+
* Convert a sequence of points to an SVG path `d` attribute string using
|
|
31521
|
+
* M + L commands (no bezier smoothing — matches the React original).
|
|
31522
|
+
*
|
|
31523
|
+
* Returns an empty string for an empty point array.
|
|
31524
|
+
*
|
|
31525
|
+
* @example
|
|
31526
|
+
* buildPathD([{x:0,y:0},{x:10,y:5}]) // "M 0 0 L 10 5"
|
|
31527
|
+
*/
|
|
31528
|
+
function buildPathD(points) {
|
|
31529
|
+
if (points.length === 0) {
|
|
31530
|
+
return '';
|
|
31531
|
+
}
|
|
31532
|
+
const first = points[0];
|
|
31533
|
+
let d = `M ${first.x} ${first.y}`;
|
|
31534
|
+
for (let i = 1; i < points.length; i++) {
|
|
31535
|
+
const pt = points[i];
|
|
31536
|
+
d += ` L ${pt.x} ${pt.y}`;
|
|
31537
|
+
}
|
|
31538
|
+
return d;
|
|
31539
|
+
}
|
|
31540
|
+
// ---------------------------------------------------------------------------
|
|
31541
|
+
// Point smoothing
|
|
31542
|
+
// ---------------------------------------------------------------------------
|
|
31543
|
+
/**
|
|
31544
|
+
* Apply a simple sliding-window average to a series of points, reducing
|
|
31545
|
+
* jitter without imposing bezier curve complexity.
|
|
31546
|
+
*
|
|
31547
|
+
* A window size of 1 is a no-op (identity). The first and last points are
|
|
31548
|
+
* always preserved as-is.
|
|
31549
|
+
*
|
|
31550
|
+
* @param points Raw pointer-event coordinates.
|
|
31551
|
+
* @param window Number of neighbours on each side to average (default 2).
|
|
31552
|
+
*/
|
|
31553
|
+
function smoothPoints(points, window = 2) {
|
|
31554
|
+
const n = points.length;
|
|
31555
|
+
if (n <= 2 || window <= 0) {
|
|
31556
|
+
return points;
|
|
31557
|
+
}
|
|
31558
|
+
const out = [];
|
|
31559
|
+
for (let i = 0; i < n; i++) {
|
|
31560
|
+
if (i === 0 || i === n - 1) {
|
|
31561
|
+
out.push(points[i]);
|
|
31562
|
+
continue;
|
|
31563
|
+
}
|
|
31564
|
+
const lo = Math.max(0, i - window);
|
|
31565
|
+
const hi = Math.min(n - 1, i + window);
|
|
31566
|
+
let sx = 0;
|
|
31567
|
+
let sy = 0;
|
|
31568
|
+
const count = hi - lo + 1;
|
|
31569
|
+
for (let j = lo; j <= hi; j++) {
|
|
31570
|
+
sx += points[j].x;
|
|
31571
|
+
sy += points[j].y;
|
|
31572
|
+
}
|
|
31573
|
+
out.push({ x: sx / count, y: sy / count });
|
|
31574
|
+
}
|
|
31575
|
+
return out;
|
|
31576
|
+
}
|
|
31577
|
+
// ---------------------------------------------------------------------------
|
|
31578
|
+
// Eraser hit-testing
|
|
31579
|
+
// ---------------------------------------------------------------------------
|
|
31580
|
+
/**
|
|
31581
|
+
* Return `true` when any point on `stroke` lies within `radius` pixels of the
|
|
31582
|
+
* eraser centre `(ex, ey)` in slide space.
|
|
31583
|
+
*
|
|
31584
|
+
* Uses squared-distance comparison to avoid Math.sqrt.
|
|
31585
|
+
*/
|
|
31586
|
+
function strokeHitsEraser(stroke, ex, ey, radius) {
|
|
31587
|
+
const r2 = radius * radius;
|
|
31588
|
+
for (const pt of stroke.points) {
|
|
31589
|
+
const dx = pt.x - ex;
|
|
31590
|
+
const dy = pt.y - ey;
|
|
31591
|
+
if (dx * dx + dy * dy < r2) {
|
|
31592
|
+
return true;
|
|
31593
|
+
}
|
|
31594
|
+
}
|
|
31595
|
+
return false;
|
|
31596
|
+
}
|
|
31597
|
+
/**
|
|
31598
|
+
* Filter `strokes`, removing any that hit the eraser at `(ex, ey)`.
|
|
31599
|
+
* Returns a new array; does not mutate the input.
|
|
31600
|
+
*/
|
|
31601
|
+
function eraseAtPoint(strokes, ex, ey, radius = ERASER_RADIUS) {
|
|
31602
|
+
return strokes.filter((s) => !strokeHitsEraser(s, ex, ey, radius));
|
|
31603
|
+
}
|
|
31604
|
+
// ---------------------------------------------------------------------------
|
|
31605
|
+
// Laser dot fade
|
|
31606
|
+
// ---------------------------------------------------------------------------
|
|
31607
|
+
/**
|
|
31608
|
+
* Compute the CSS `opacity` value for a fading laser dot given a fade ratio
|
|
31609
|
+
* in [0, 1] where 0 = fully visible and 1 = fully invisible.
|
|
31610
|
+
*
|
|
31611
|
+
* Uses a simple quadratic ease-out so the dot lingers at full intensity then
|
|
31612
|
+
* fades quickly near the end.
|
|
31613
|
+
*
|
|
31614
|
+
* @param ratio 0 (fresh) → 1 (expired)
|
|
31615
|
+
*/
|
|
31616
|
+
function laserDotOpacity(ratio) {
|
|
31617
|
+
const clamped = Math.max(0, Math.min(1, ratio));
|
|
31618
|
+
return (1 - clamped) * (1 - clamped);
|
|
31619
|
+
}
|
|
31620
|
+
// ---------------------------------------------------------------------------
|
|
31621
|
+
// Cursor helper
|
|
31622
|
+
// ---------------------------------------------------------------------------
|
|
31623
|
+
/**
|
|
31624
|
+
* Return the CSS `cursor` value that matches `tool`.
|
|
31625
|
+
*/
|
|
31626
|
+
function cursorForTool(tool) {
|
|
31627
|
+
switch (tool) {
|
|
31628
|
+
case 'laser':
|
|
31629
|
+
return 'none';
|
|
31630
|
+
case 'pen':
|
|
31631
|
+
return 'crosshair';
|
|
31632
|
+
case 'highlighter':
|
|
31633
|
+
return 'crosshair';
|
|
31634
|
+
case 'eraser':
|
|
31635
|
+
return 'crosshair';
|
|
31636
|
+
default:
|
|
31637
|
+
return 'default';
|
|
31638
|
+
}
|
|
31639
|
+
}
|
|
31640
|
+
|
|
31641
|
+
/**
|
|
31642
|
+
* presentation-annotations.service.ts — Signal-based service that manages ink
|
|
31643
|
+
* annotation state during presentation mode.
|
|
31644
|
+
*
|
|
31645
|
+
* Ported from React:
|
|
31646
|
+
* packages/react/src/viewer/hooks/usePresentationAnnotations.ts
|
|
31647
|
+
* packages/react/src/viewer/hooks/usePresentationAnnotations.types.ts
|
|
31648
|
+
*
|
|
31649
|
+
* Provides: pen / highlighter / eraser / laser-pointer tools, per-slide stroke
|
|
31650
|
+
* storage keyed by slide index, begin / extend / end stroke lifecycle, undo
|
|
31651
|
+
* (single-step: clear current slide), clear-all, and a transient laser position.
|
|
31652
|
+
*
|
|
31653
|
+
* Provide at the component level so lifetime tracks the host overlay:
|
|
31654
|
+
* `@Component({ providers: [PresentationAnnotationsService] })`.
|
|
31655
|
+
*/
|
|
31656
|
+
class PresentationAnnotationsService {
|
|
31657
|
+
destroyRef = inject(DestroyRef);
|
|
31658
|
+
// ------------------------------------------------------------------
|
|
31659
|
+
// Private signals
|
|
31660
|
+
// ------------------------------------------------------------------
|
|
31661
|
+
_tool = signal('none', /* @ts-ignore */
|
|
31662
|
+
...(ngDevMode ? [{ debugName: "_tool" }] : /* istanbul ignore next */ []));
|
|
31663
|
+
_penColor = signal('#ff0000', /* @ts-ignore */
|
|
31664
|
+
...(ngDevMode ? [{ debugName: "_penColor" }] : /* istanbul ignore next */ []));
|
|
31665
|
+
_highlighterColor = signal('#ffff00', /* @ts-ignore */
|
|
31666
|
+
...(ngDevMode ? [{ debugName: "_highlighterColor" }] : /* istanbul ignore next */ []));
|
|
31667
|
+
_currentStroke = signal(null, /* @ts-ignore */
|
|
31668
|
+
...(ngDevMode ? [{ debugName: "_currentStroke" }] : /* istanbul ignore next */ []));
|
|
31669
|
+
_laserPosition = signal(null, /* @ts-ignore */
|
|
31670
|
+
...(ngDevMode ? [{ debugName: "_laserPosition" }] : /* istanbul ignore next */ []));
|
|
31671
|
+
_toolbarVisible = signal(false, /* @ts-ignore */
|
|
31672
|
+
...(ngDevMode ? [{ debugName: "_toolbarVisible" }] : /* istanbul ignore next */ []));
|
|
31673
|
+
/**
|
|
31674
|
+
* Strokes visible on the current slide. Cleared / populated whenever
|
|
31675
|
+
* `setActiveSlide()` is called.
|
|
31676
|
+
*/
|
|
31677
|
+
_annotationStrokes = signal([], /* @ts-ignore */
|
|
31678
|
+
...(ngDevMode ? [{ debugName: "_annotationStrokes" }] : /* istanbul ignore next */ []));
|
|
31679
|
+
/**
|
|
31680
|
+
* Per-slide annotation storage. Strokes for the current slide are also kept
|
|
31681
|
+
* in `_annotationStrokes` for rendering convenience; they are synchronised
|
|
31682
|
+
* back into this map on `setActiveSlide()` or `endStroke()`.
|
|
31683
|
+
*/
|
|
31684
|
+
_slideAnnotations = new Map();
|
|
31685
|
+
/** Tracks whether a pointer-down initiated an erase gesture. */
|
|
31686
|
+
_isErasing = false;
|
|
31687
|
+
/** Toolbar auto-hide timer handle. */
|
|
31688
|
+
_toolbarTimer = null;
|
|
31689
|
+
/** Index of the slide currently being annotated. */
|
|
31690
|
+
_activeSlideIndex = 0;
|
|
31691
|
+
// ------------------------------------------------------------------
|
|
31692
|
+
// Public read-only signal surface
|
|
31693
|
+
// ------------------------------------------------------------------
|
|
31694
|
+
/** The currently-armed presentation tool. */
|
|
31695
|
+
tool = computed(() => this._tool(), /* @ts-ignore */
|
|
31696
|
+
...(ngDevMode ? [{ debugName: "tool" }] : /* istanbul ignore next */ []));
|
|
31697
|
+
/** Stroke colour used by the pen tool. */
|
|
31698
|
+
penColor = computed(() => this._penColor(), /* @ts-ignore */
|
|
31699
|
+
...(ngDevMode ? [{ debugName: "penColor" }] : /* istanbul ignore next */ []));
|
|
31700
|
+
/** Stroke colour used by the highlighter tool. */
|
|
31701
|
+
highlighterColor = computed(() => this._highlighterColor(), /* @ts-ignore */
|
|
31702
|
+
...(ngDevMode ? [{ debugName: "highlighterColor" }] : /* istanbul ignore next */ []));
|
|
31703
|
+
/** Committed strokes on the active slide (does not include the live stroke). */
|
|
31704
|
+
annotationStrokes = computed(() => this._annotationStrokes(), /* @ts-ignore */
|
|
31705
|
+
...(ngDevMode ? [{ debugName: "annotationStrokes" }] : /* istanbul ignore next */ []));
|
|
31706
|
+
/** The stroke currently being drawn, or null when not drawing. */
|
|
31707
|
+
currentStroke = computed(() => this._currentStroke(), /* @ts-ignore */
|
|
31708
|
+
...(ngDevMode ? [{ debugName: "currentStroke" }] : /* istanbul ignore next */ []));
|
|
31709
|
+
/** Laser pointer position in slide-space pixels, or null when not visible. */
|
|
31710
|
+
laserPosition = computed(() => this._laserPosition(), /* @ts-ignore */
|
|
31711
|
+
...(ngDevMode ? [{ debugName: "laserPosition" }] : /* istanbul ignore next */ []));
|
|
31712
|
+
/** Whether the annotation toolbar should be visible. */
|
|
31713
|
+
toolbarVisible = computed(() => this._toolbarVisible(), /* @ts-ignore */
|
|
31714
|
+
...(ngDevMode ? [{ debugName: "toolbarVisible" }] : /* istanbul ignore next */ []));
|
|
31715
|
+
/**
|
|
31716
|
+
* `true` when any annotations exist on any slide (used for the
|
|
31717
|
+
* "keep / discard" dialog when exiting presentation mode).
|
|
31718
|
+
*/
|
|
31719
|
+
hasAnyAnnotations = computed(() => this._annotationStrokes().length > 0 || this._slideAnnotations.size > 0, /* @ts-ignore */
|
|
31720
|
+
...(ngDevMode ? [{ debugName: "hasAnyAnnotations" }] : /* istanbul ignore next */ []));
|
|
31721
|
+
// ------------------------------------------------------------------
|
|
31722
|
+
// Lifecycle
|
|
31723
|
+
// ------------------------------------------------------------------
|
|
31724
|
+
constructor() {
|
|
31725
|
+
this.destroyRef.onDestroy(() => {
|
|
31726
|
+
this._clearToolbarTimer();
|
|
31727
|
+
});
|
|
31728
|
+
}
|
|
31729
|
+
// ------------------------------------------------------------------
|
|
31730
|
+
// Tool management
|
|
31731
|
+
// ------------------------------------------------------------------
|
|
31732
|
+
/**
|
|
31733
|
+
* Arm `tool`. If it is already armed, toggle back to `'none'` (matches
|
|
31734
|
+
* the React behaviour of `setPresentationTool`).
|
|
31735
|
+
*/
|
|
31736
|
+
setTool(tool) {
|
|
31737
|
+
this._tool.update((current) => (current === tool ? 'none' : tool));
|
|
31738
|
+
// Clear transient state when switching away from drawing tools.
|
|
31739
|
+
if (this._tool() === 'none' || this._tool() === 'laser') {
|
|
31740
|
+
this._currentStroke.set(null);
|
|
31741
|
+
this._isErasing = false;
|
|
31742
|
+
}
|
|
31743
|
+
if (this._tool() !== 'laser') {
|
|
31744
|
+
this._laserPosition.set(null);
|
|
31745
|
+
}
|
|
31746
|
+
}
|
|
31747
|
+
/** Set the pen colour directly (without toggling the tool). */
|
|
31748
|
+
setPenColor(color) {
|
|
31749
|
+
this._penColor.set(color);
|
|
31750
|
+
}
|
|
31751
|
+
/** Set the highlighter colour directly. */
|
|
31752
|
+
setHighlighterColor(color) {
|
|
31753
|
+
this._highlighterColor.set(color);
|
|
31754
|
+
}
|
|
31755
|
+
/** Show / hide the toolbar programmatically. */
|
|
31756
|
+
setToolbarVisible(visible) {
|
|
31757
|
+
this._toolbarVisible.set(visible);
|
|
31758
|
+
}
|
|
31759
|
+
// ------------------------------------------------------------------
|
|
31760
|
+
// Active slide management
|
|
31761
|
+
// ------------------------------------------------------------------
|
|
31762
|
+
/**
|
|
31763
|
+
* Notify the service that the visible slide has changed. Saves the
|
|
31764
|
+
* in-progress strokes for the previous slide and loads the strokes
|
|
31765
|
+
* for `newIndex`.
|
|
31766
|
+
*/
|
|
31767
|
+
setActiveSlide(newIndex) {
|
|
31768
|
+
if (newIndex === this._activeSlideIndex) {
|
|
31769
|
+
return;
|
|
31770
|
+
}
|
|
31771
|
+
this._flushCurrentSlide();
|
|
31772
|
+
this._activeSlideIndex = newIndex;
|
|
31773
|
+
const existing = this._slideAnnotations.get(newIndex) ?? [];
|
|
31774
|
+
this._annotationStrokes.set([...existing]);
|
|
31775
|
+
this._currentStroke.set(null);
|
|
31776
|
+
this._isErasing = false;
|
|
31777
|
+
}
|
|
31778
|
+
// ------------------------------------------------------------------
|
|
31779
|
+
// Drawing
|
|
31780
|
+
// ------------------------------------------------------------------
|
|
31781
|
+
/**
|
|
31782
|
+
* Begin a stroke at `(x, y)` in slide-space coordinates.
|
|
31783
|
+
* Called on pointer-down when the pen or highlighter is armed.
|
|
31784
|
+
*/
|
|
31785
|
+
beginStroke(x, y) {
|
|
31786
|
+
const tool = this._tool();
|
|
31787
|
+
if (tool !== 'pen' && tool !== 'highlighter') {
|
|
31788
|
+
return;
|
|
31789
|
+
}
|
|
31790
|
+
const isPen = tool === 'pen';
|
|
31791
|
+
this._currentStroke.set({
|
|
31792
|
+
id: nextStrokeId(),
|
|
31793
|
+
points: [{ x, y }],
|
|
31794
|
+
color: isPen ? this._penColor() : this._highlighterColor(),
|
|
31795
|
+
width: isPen ? PEN_WIDTH : HIGHLIGHTER_WIDTH,
|
|
31796
|
+
opacity: isPen ? 1 : HIGHLIGHTER_OPACITY,
|
|
31797
|
+
});
|
|
31798
|
+
}
|
|
31799
|
+
/**
|
|
31800
|
+
* Extend the active stroke by appending `(x, y)`.
|
|
31801
|
+
* Called on pointer-move while drawing.
|
|
31802
|
+
*/
|
|
31803
|
+
extendStroke(x, y) {
|
|
31804
|
+
this._currentStroke.update((prev) => {
|
|
31805
|
+
if (!prev) {
|
|
31806
|
+
return null;
|
|
31807
|
+
}
|
|
31808
|
+
return { ...prev, points: [...prev.points, { x, y }] };
|
|
31809
|
+
});
|
|
31810
|
+
}
|
|
31811
|
+
/**
|
|
31812
|
+
* Commit the active stroke to the slide's annotation list.
|
|
31813
|
+
* Called on pointer-up or pointer-leave while drawing.
|
|
31814
|
+
* Strokes with fewer than 2 points are discarded.
|
|
31815
|
+
*/
|
|
31816
|
+
endStroke() {
|
|
31817
|
+
const stroke = this._currentStroke();
|
|
31818
|
+
this._currentStroke.set(null);
|
|
31819
|
+
if (!stroke || stroke.points.length < 2) {
|
|
31820
|
+
return;
|
|
31821
|
+
}
|
|
31822
|
+
this._annotationStrokes.update((prev) => {
|
|
31823
|
+
// Guard against duplicate ids (shouldn't happen, but matches React safety).
|
|
31824
|
+
if (prev.some((s) => s.id === stroke.id)) {
|
|
31825
|
+
return prev;
|
|
31826
|
+
}
|
|
31827
|
+
const updated = [...prev, stroke];
|
|
31828
|
+
this._slideAnnotations.set(this._activeSlideIndex, updated);
|
|
31829
|
+
return updated;
|
|
31830
|
+
});
|
|
31831
|
+
}
|
|
31832
|
+
// ------------------------------------------------------------------
|
|
31833
|
+
// Eraser
|
|
31834
|
+
// ------------------------------------------------------------------
|
|
31835
|
+
/**
|
|
31836
|
+
* Begin an eraser gesture at `(x, y)`.
|
|
31837
|
+
* Must be called on pointer-down when the eraser is armed.
|
|
31838
|
+
*/
|
|
31839
|
+
beginErase(x, y) {
|
|
31840
|
+
if (this._tool() !== 'eraser') {
|
|
31841
|
+
return;
|
|
31842
|
+
}
|
|
31843
|
+
this._isErasing = true;
|
|
31844
|
+
this._applyErase(x, y);
|
|
31845
|
+
}
|
|
31846
|
+
/**
|
|
31847
|
+
* Continue erasing at `(x, y)` during a pointer-move.
|
|
31848
|
+
*/
|
|
31849
|
+
continueErase(x, y) {
|
|
31850
|
+
if (!this._isErasing || this._tool() !== 'eraser') {
|
|
31851
|
+
return;
|
|
31852
|
+
}
|
|
31853
|
+
this._applyErase(x, y);
|
|
31854
|
+
}
|
|
31855
|
+
/**
|
|
31856
|
+
* End an eraser gesture. Called on pointer-up or pointer-leave.
|
|
31857
|
+
*/
|
|
31858
|
+
endErase() {
|
|
31859
|
+
this._isErasing = false;
|
|
31860
|
+
}
|
|
31861
|
+
// ------------------------------------------------------------------
|
|
31862
|
+
// Laser
|
|
31863
|
+
// ------------------------------------------------------------------
|
|
31864
|
+
/**
|
|
31865
|
+
* Update the laser dot position (slide-space coords).
|
|
31866
|
+
* Only takes effect when the laser tool is armed.
|
|
31867
|
+
*/
|
|
31868
|
+
moveLaser(x, y) {
|
|
31869
|
+
if (this._tool() !== 'laser') {
|
|
31870
|
+
return;
|
|
31871
|
+
}
|
|
31872
|
+
this._laserPosition.set({ x, y });
|
|
31873
|
+
}
|
|
31874
|
+
/**
|
|
31875
|
+
* Hide the laser dot (called on pointer-leave).
|
|
31876
|
+
*/
|
|
31877
|
+
hideLaser() {
|
|
31878
|
+
this._laserPosition.set(null);
|
|
31879
|
+
}
|
|
31880
|
+
// ------------------------------------------------------------------
|
|
31881
|
+
// Clear / undo
|
|
31882
|
+
// ------------------------------------------------------------------
|
|
31883
|
+
/**
|
|
31884
|
+
* Clear all annotations on the active slide.
|
|
31885
|
+
*/
|
|
31886
|
+
clearAnnotations() {
|
|
31887
|
+
this._annotationStrokes.set([]);
|
|
31888
|
+
this._currentStroke.set(null);
|
|
31889
|
+
this._isErasing = false;
|
|
31890
|
+
this._slideAnnotations.delete(this._activeSlideIndex);
|
|
31891
|
+
}
|
|
31892
|
+
/**
|
|
31893
|
+
* Clear all annotations across every slide.
|
|
31894
|
+
*/
|
|
31895
|
+
clearAllAnnotations() {
|
|
31896
|
+
this._annotationStrokes.set([]);
|
|
31897
|
+
this._currentStroke.set(null);
|
|
31898
|
+
this._isErasing = false;
|
|
31899
|
+
this._slideAnnotations.clear();
|
|
31900
|
+
}
|
|
31901
|
+
// ------------------------------------------------------------------
|
|
31902
|
+
// Snapshot
|
|
31903
|
+
// ------------------------------------------------------------------
|
|
31904
|
+
/**
|
|
31905
|
+
* Return a snapshot of all annotations across every slide.
|
|
31906
|
+
* The current slide's in-progress committed strokes are folded in.
|
|
31907
|
+
*
|
|
31908
|
+
* Used by the "keep as ink elements" dialog handler.
|
|
31909
|
+
*/
|
|
31910
|
+
getAllSlideAnnotations() {
|
|
31911
|
+
this._flushCurrentSlide();
|
|
31912
|
+
return new Map(this._slideAnnotations);
|
|
31913
|
+
}
|
|
31914
|
+
// ------------------------------------------------------------------
|
|
31915
|
+
// Toolbar auto-hide
|
|
31916
|
+
// ------------------------------------------------------------------
|
|
31917
|
+
/**
|
|
31918
|
+
* Show the toolbar and schedule it to auto-hide after `delayMs` (default 3 s).
|
|
31919
|
+
* Resets the timer if called repeatedly (debounce on mouse-move).
|
|
31920
|
+
*/
|
|
31921
|
+
showToolbarTemporarily(delayMs = 3000) {
|
|
31922
|
+
this._toolbarVisible.set(true);
|
|
31923
|
+
this._clearToolbarTimer();
|
|
31924
|
+
this._toolbarTimer = setTimeout(() => {
|
|
31925
|
+
this._toolbarVisible.set(false);
|
|
31926
|
+
this._toolbarTimer = null;
|
|
31927
|
+
}, delayMs);
|
|
31928
|
+
}
|
|
31929
|
+
// ------------------------------------------------------------------
|
|
31930
|
+
// Reset on exit
|
|
31931
|
+
// ------------------------------------------------------------------
|
|
31932
|
+
/**
|
|
31933
|
+
* Reset transient presentation state (tool, current stroke, laser, toolbar
|
|
31934
|
+
* timer). Does NOT clear stored strokes — those persist for the
|
|
31935
|
+
* keep/discard dialog.
|
|
31936
|
+
*/
|
|
31937
|
+
resetForExit() {
|
|
31938
|
+
this._tool.set('none');
|
|
31939
|
+
this._currentStroke.set(null);
|
|
31940
|
+
this._laserPosition.set(null);
|
|
31941
|
+
this._isErasing = false;
|
|
31942
|
+
this._toolbarVisible.set(false);
|
|
31943
|
+
this._clearToolbarTimer();
|
|
31944
|
+
}
|
|
31945
|
+
// ------------------------------------------------------------------
|
|
31946
|
+
// Private helpers
|
|
31947
|
+
// ------------------------------------------------------------------
|
|
31948
|
+
_applyErase(x, y) {
|
|
31949
|
+
this._annotationStrokes.update((prev) => {
|
|
31950
|
+
const next = eraseAtPoint(prev, x, y, ERASER_RADIUS);
|
|
31951
|
+
if (next.length > 0) {
|
|
31952
|
+
this._slideAnnotations.set(this._activeSlideIndex, next);
|
|
31953
|
+
}
|
|
31954
|
+
else {
|
|
31955
|
+
this._slideAnnotations.delete(this._activeSlideIndex);
|
|
31956
|
+
}
|
|
31957
|
+
return next;
|
|
31958
|
+
});
|
|
31959
|
+
}
|
|
31960
|
+
_flushCurrentSlide() {
|
|
31961
|
+
const strokes = this._annotationStrokes();
|
|
31962
|
+
if (strokes.length > 0) {
|
|
31963
|
+
this._slideAnnotations.set(this._activeSlideIndex, [...strokes]);
|
|
31964
|
+
}
|
|
31965
|
+
else {
|
|
31966
|
+
this._slideAnnotations.delete(this._activeSlideIndex);
|
|
31967
|
+
}
|
|
31968
|
+
}
|
|
31969
|
+
_clearToolbarTimer() {
|
|
31970
|
+
if (this._toolbarTimer !== null) {
|
|
31971
|
+
clearTimeout(this._toolbarTimer);
|
|
31972
|
+
this._toolbarTimer = null;
|
|
31973
|
+
}
|
|
31974
|
+
}
|
|
31975
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationAnnotationsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
31976
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationAnnotationsService });
|
|
31977
|
+
}
|
|
31978
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationAnnotationsService, decorators: [{
|
|
31979
|
+
type: Injectable
|
|
31980
|
+
}], ctorParameters: () => [] });
|
|
31981
|
+
|
|
31982
|
+
/**
|
|
31983
|
+
* presentation-annotation-overlay.component.ts — SVG overlay that captures
|
|
31984
|
+
* pointer events and renders ink strokes + laser dot during presentation mode.
|
|
31985
|
+
*
|
|
31986
|
+
* Ported from React:
|
|
31987
|
+
* packages/react/src/viewer/components/PresentationAnnotationOverlay.tsx
|
|
31988
|
+
*
|
|
31989
|
+
* Selector: `pptx-presentation-annotation-overlay`
|
|
31990
|
+
*
|
|
31991
|
+
* This component is purely presentational: it calls out to
|
|
31992
|
+
* {@link PresentationAnnotationsService} for all state mutations, and
|
|
31993
|
+
* accepts `canvasSize` and `zoom` to correctly map pointer events from
|
|
31994
|
+
* screen space into slide space.
|
|
31995
|
+
*
|
|
31996
|
+
* When the tool is `'none'` the component renders nothing and sets
|
|
31997
|
+
* `pointer-events: none` on its host so click-to-advance still works.
|
|
31998
|
+
*/
|
|
31999
|
+
class PresentationAnnotationOverlayComponent {
|
|
32000
|
+
// ------------------------------------------------------------------
|
|
32001
|
+
// Inputs
|
|
32002
|
+
// ------------------------------------------------------------------
|
|
32003
|
+
/** Logical canvas dimensions (the slide's authored size in pixels). */
|
|
32004
|
+
canvasSize = input.required(/* @ts-ignore */
|
|
32005
|
+
...(ngDevMode ? [{ debugName: "canvasSize" }] : /* istanbul ignore next */ []));
|
|
32006
|
+
/**
|
|
32007
|
+
* The zoom factor currently applied to the slide stage.
|
|
32008
|
+
* Pointer-event coordinates are divided by this value to obtain
|
|
32009
|
+
* slide-space coordinates.
|
|
32010
|
+
*/
|
|
32011
|
+
zoom = input(1, /* @ts-ignore */
|
|
32012
|
+
...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
32013
|
+
// ------------------------------------------------------------------
|
|
32014
|
+
// Injected state
|
|
32015
|
+
// ------------------------------------------------------------------
|
|
32016
|
+
service = inject(PresentationAnnotationsService);
|
|
32017
|
+
// ------------------------------------------------------------------
|
|
32018
|
+
// Template references
|
|
32019
|
+
// ------------------------------------------------------------------
|
|
32020
|
+
svgRef = viewChild('svg', /* @ts-ignore */
|
|
32021
|
+
...(ngDevMode ? [{ debugName: "svgRef" }] : /* istanbul ignore next */ []));
|
|
32022
|
+
// ------------------------------------------------------------------
|
|
32023
|
+
// Eraser bookkeeping
|
|
32024
|
+
// ------------------------------------------------------------------
|
|
32025
|
+
/** True while a pointer-down is active in eraser mode. */
|
|
32026
|
+
_isErasing = false;
|
|
32027
|
+
// ------------------------------------------------------------------
|
|
32028
|
+
// Derived signals
|
|
32029
|
+
// ------------------------------------------------------------------
|
|
32030
|
+
/** True when any tool other than 'none' is armed. */
|
|
32031
|
+
isArmed = computed(() => this.service.tool() !== 'none', /* @ts-ignore */
|
|
32032
|
+
...(ngDevMode ? [{ debugName: "isArmed" }] : /* istanbul ignore next */ []));
|
|
32033
|
+
/** SVG viewBox string that covers the full canvas. */
|
|
32034
|
+
viewBox = computed(() => {
|
|
32035
|
+
const { width, height } = this.canvasSize();
|
|
32036
|
+
return `0 0 ${width} ${height}`;
|
|
32037
|
+
}, /* @ts-ignore */
|
|
32038
|
+
...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
|
|
32039
|
+
/** CSS cursor for the outer wrapper div. */
|
|
32040
|
+
wrapperStyle = computed(() => ({
|
|
32041
|
+
cursor: cursorForTool(this.service.tool()),
|
|
32042
|
+
'pointer-events': this.isArmed() ? 'auto' : 'none',
|
|
32043
|
+
'z-index': '60',
|
|
32044
|
+
}), /* @ts-ignore */
|
|
32045
|
+
...(ngDevMode ? [{ debugName: "wrapperStyle" }] : /* istanbul ignore next */ []));
|
|
32046
|
+
/** Transform the SVG to match the slide's zoom level. */
|
|
32047
|
+
svgStyle = computed(() => ({
|
|
32048
|
+
transform: `scale(${this.zoom()})`,
|
|
32049
|
+
}), /* @ts-ignore */
|
|
32050
|
+
...(ngDevMode ? [{ debugName: "svgStyle" }] : /* istanbul ignore next */ []));
|
|
32051
|
+
/** All strokes to render: committed + the live in-progress stroke. */
|
|
32052
|
+
allStrokes = computed(() => {
|
|
32053
|
+
const committed = this.service.annotationStrokes();
|
|
32054
|
+
const live = this.service.currentStroke();
|
|
32055
|
+
return live ? [...committed, live] : committed;
|
|
32056
|
+
}, /* @ts-ignore */
|
|
32057
|
+
...(ngDevMode ? [{ debugName: "allStrokes" }] : /* istanbul ignore next */ []));
|
|
32058
|
+
// ------------------------------------------------------------------
|
|
32059
|
+
// Template helpers
|
|
32060
|
+
// ------------------------------------------------------------------
|
|
32061
|
+
strokePath(points) {
|
|
32062
|
+
if (points.length === 0) {
|
|
32063
|
+
return '';
|
|
32064
|
+
}
|
|
32065
|
+
const first = points[0];
|
|
32066
|
+
let d = `M ${first.x} ${first.y}`;
|
|
32067
|
+
for (let i = 1; i < points.length; i++) {
|
|
32068
|
+
const pt = points[i];
|
|
32069
|
+
d += ` L ${pt.x} ${pt.y}`;
|
|
32070
|
+
}
|
|
32071
|
+
return d;
|
|
32072
|
+
}
|
|
32073
|
+
laserDotStyle(x, y) {
|
|
32074
|
+
const z = this.zoom();
|
|
32075
|
+
const dotSize = 24;
|
|
32076
|
+
return {
|
|
32077
|
+
width: `${dotSize}px`,
|
|
32078
|
+
height: `${dotSize}px`,
|
|
32079
|
+
left: `${x * z - dotSize / 2}px`,
|
|
32080
|
+
top: `${y * z - dotSize / 2}px`,
|
|
32081
|
+
'z-index': '70',
|
|
32082
|
+
};
|
|
32083
|
+
}
|
|
32084
|
+
// ------------------------------------------------------------------
|
|
32085
|
+
// Pointer event dispatch
|
|
32086
|
+
// ------------------------------------------------------------------
|
|
32087
|
+
onPointerDown(event) {
|
|
32088
|
+
const tool = this.service.tool();
|
|
32089
|
+
if (tool === 'none') {
|
|
32090
|
+
return;
|
|
32091
|
+
}
|
|
32092
|
+
event.preventDefault();
|
|
32093
|
+
event.stopPropagation();
|
|
32094
|
+
const coords = this._toSlideCoords(event.clientX, event.clientY);
|
|
32095
|
+
if (!coords) {
|
|
32096
|
+
return;
|
|
32097
|
+
}
|
|
32098
|
+
if (tool === 'eraser') {
|
|
32099
|
+
this._isErasing = true;
|
|
32100
|
+
this.service.beginErase(coords.x, coords.y);
|
|
32101
|
+
return;
|
|
32102
|
+
}
|
|
32103
|
+
if (tool === 'pen' || tool === 'highlighter') {
|
|
32104
|
+
this.service.beginStroke(coords.x, coords.y);
|
|
32105
|
+
}
|
|
32106
|
+
}
|
|
32107
|
+
onPointerMove(event) {
|
|
32108
|
+
const tool = this.service.tool();
|
|
32109
|
+
if (tool === 'none') {
|
|
32110
|
+
return;
|
|
32111
|
+
}
|
|
32112
|
+
const coords = this._toSlideCoords(event.clientX, event.clientY);
|
|
32113
|
+
if (!coords) {
|
|
32114
|
+
return;
|
|
32115
|
+
}
|
|
32116
|
+
if (tool === 'laser') {
|
|
32117
|
+
this.service.moveLaser(coords.x, coords.y);
|
|
32118
|
+
return;
|
|
32119
|
+
}
|
|
32120
|
+
if (tool === 'eraser' && this._isErasing) {
|
|
32121
|
+
this.service.continueErase(coords.x, coords.y);
|
|
32122
|
+
return;
|
|
32123
|
+
}
|
|
32124
|
+
if (tool === 'pen' || tool === 'highlighter') {
|
|
32125
|
+
this.service.extendStroke(coords.x, coords.y);
|
|
32126
|
+
}
|
|
32127
|
+
}
|
|
32128
|
+
onPointerUp(event) {
|
|
32129
|
+
const tool = this.service.tool();
|
|
32130
|
+
if (tool === 'none') {
|
|
32131
|
+
return;
|
|
32132
|
+
}
|
|
32133
|
+
event.preventDefault();
|
|
32134
|
+
if (tool === 'eraser') {
|
|
32135
|
+
this._isErasing = false;
|
|
32136
|
+
this.service.endErase();
|
|
32137
|
+
return;
|
|
32138
|
+
}
|
|
32139
|
+
this.service.endStroke();
|
|
32140
|
+
}
|
|
32141
|
+
onPointerLeave(_event) {
|
|
32142
|
+
const tool = this.service.tool();
|
|
32143
|
+
if (tool === 'laser') {
|
|
32144
|
+
this.service.hideLaser();
|
|
32145
|
+
}
|
|
32146
|
+
if (tool === 'eraser') {
|
|
32147
|
+
this._isErasing = false;
|
|
32148
|
+
this.service.endErase();
|
|
32149
|
+
}
|
|
32150
|
+
this.service.endStroke();
|
|
32151
|
+
}
|
|
32152
|
+
// ------------------------------------------------------------------
|
|
32153
|
+
// Coordinate mapping
|
|
32154
|
+
// ------------------------------------------------------------------
|
|
32155
|
+
/**
|
|
32156
|
+
* Map a client-space pointer position to slide-space coordinates by
|
|
32157
|
+
* subtracting the SVG element's bounding rect and dividing by the zoom.
|
|
32158
|
+
* Returns `null` when the SVG ref is not yet available.
|
|
32159
|
+
*/
|
|
32160
|
+
_toSlideCoords(clientX, clientY) {
|
|
32161
|
+
const svgEl = this.svgRef()?.nativeElement;
|
|
32162
|
+
if (!svgEl) {
|
|
32163
|
+
return null;
|
|
32164
|
+
}
|
|
32165
|
+
const rect = svgEl.getBoundingClientRect();
|
|
32166
|
+
const z = this.zoom() || 1;
|
|
32167
|
+
return {
|
|
32168
|
+
x: (clientX - rect.left) / z,
|
|
32169
|
+
y: (clientY - rect.top) / z,
|
|
32170
|
+
};
|
|
32171
|
+
}
|
|
32172
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationAnnotationOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
32173
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: PresentationAnnotationOverlayComponent, isStandalone: true, selector: "pptx-presentation-annotation-overlay", inputs: { canvasSize: { classPropertyName: "canvasSize", publicName: "canvasSize", isSignal: true, isRequired: true, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "svgRef", first: true, predicate: ["svg"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
32174
|
+
@if (isArmed()) {
|
|
32175
|
+
<div
|
|
32176
|
+
class="pptx-ng-anno-wrapper"
|
|
32177
|
+
[ngStyle]="wrapperStyle()"
|
|
32178
|
+
(pointerdown)="onPointerDown($event)"
|
|
32179
|
+
(pointermove)="onPointerMove($event)"
|
|
32180
|
+
(pointerup)="onPointerUp($event)"
|
|
32181
|
+
(pointerleave)="onPointerLeave($event)"
|
|
32182
|
+
>
|
|
32183
|
+
<svg
|
|
32184
|
+
#svg
|
|
32185
|
+
class="pptx-ng-anno-svg"
|
|
32186
|
+
[attr.width]="canvasSize().width"
|
|
32187
|
+
[attr.height]="canvasSize().height"
|
|
32188
|
+
[attr.viewBox]="viewBox()"
|
|
32189
|
+
[ngStyle]="svgStyle()"
|
|
32190
|
+
>
|
|
32191
|
+
@for (s of allStrokes(); track s.id) {
|
|
32192
|
+
<path
|
|
32193
|
+
[attr.d]="strokePath(s.points)"
|
|
32194
|
+
fill="none"
|
|
32195
|
+
[attr.stroke]="s.color"
|
|
32196
|
+
[attr.stroke-width]="s.width"
|
|
32197
|
+
stroke-linecap="round"
|
|
32198
|
+
stroke-linejoin="round"
|
|
32199
|
+
[attr.opacity]="s.opacity"
|
|
32200
|
+
/>
|
|
32201
|
+
}
|
|
32202
|
+
</svg>
|
|
32203
|
+
|
|
32204
|
+
@if (service.tool() === 'laser' && service.laserPosition(); as lp) {
|
|
32205
|
+
<div class="pptx-ng-laser-dot" [ngStyle]="laserDotStyle(lp.x, lp.y)"></div>
|
|
32206
|
+
}
|
|
32207
|
+
</div>
|
|
32208
|
+
}
|
|
32209
|
+
`, isInline: true, styles: [":host{display:block;position:absolute;inset:0;pointer-events:none}.pptx-ng-anno-wrapper{position:absolute;inset:0}.pptx-ng-anno-svg{position:absolute;top:0;left:0;transform-origin:top left;overflow:visible}.pptx-ng-laser-dot{position:absolute;border-radius:50%;pointer-events:none;background:#ff0000d9;box-shadow:0 0 12px 6px #ff000080,0 0 24px 12px #ff000040;filter:drop-shadow(0 0 8px rgba(255,0,0,.7))}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
32210
|
+
}
|
|
32211
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationAnnotationOverlayComponent, decorators: [{
|
|
32212
|
+
type: Component,
|
|
32213
|
+
args: [{ selector: 'pptx-presentation-annotation-overlay', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgStyle], template: `
|
|
32214
|
+
@if (isArmed()) {
|
|
32215
|
+
<div
|
|
32216
|
+
class="pptx-ng-anno-wrapper"
|
|
32217
|
+
[ngStyle]="wrapperStyle()"
|
|
32218
|
+
(pointerdown)="onPointerDown($event)"
|
|
32219
|
+
(pointermove)="onPointerMove($event)"
|
|
32220
|
+
(pointerup)="onPointerUp($event)"
|
|
32221
|
+
(pointerleave)="onPointerLeave($event)"
|
|
32222
|
+
>
|
|
32223
|
+
<svg
|
|
32224
|
+
#svg
|
|
32225
|
+
class="pptx-ng-anno-svg"
|
|
32226
|
+
[attr.width]="canvasSize().width"
|
|
32227
|
+
[attr.height]="canvasSize().height"
|
|
32228
|
+
[attr.viewBox]="viewBox()"
|
|
32229
|
+
[ngStyle]="svgStyle()"
|
|
32230
|
+
>
|
|
32231
|
+
@for (s of allStrokes(); track s.id) {
|
|
32232
|
+
<path
|
|
32233
|
+
[attr.d]="strokePath(s.points)"
|
|
32234
|
+
fill="none"
|
|
32235
|
+
[attr.stroke]="s.color"
|
|
32236
|
+
[attr.stroke-width]="s.width"
|
|
32237
|
+
stroke-linecap="round"
|
|
32238
|
+
stroke-linejoin="round"
|
|
32239
|
+
[attr.opacity]="s.opacity"
|
|
32240
|
+
/>
|
|
32241
|
+
}
|
|
32242
|
+
</svg>
|
|
32243
|
+
|
|
32244
|
+
@if (service.tool() === 'laser' && service.laserPosition(); as lp) {
|
|
32245
|
+
<div class="pptx-ng-laser-dot" [ngStyle]="laserDotStyle(lp.x, lp.y)"></div>
|
|
32246
|
+
}
|
|
32247
|
+
</div>
|
|
32248
|
+
}
|
|
32249
|
+
`, styles: [":host{display:block;position:absolute;inset:0;pointer-events:none}.pptx-ng-anno-wrapper{position:absolute;inset:0}.pptx-ng-anno-svg{position:absolute;top:0;left:0;transform-origin:top left;overflow:visible}.pptx-ng-laser-dot{position:absolute;border-radius:50%;pointer-events:none;background:#ff0000d9;box-shadow:0 0 12px 6px #ff000080,0 0 24px 12px #ff000040;filter:drop-shadow(0 0 8px rgba(255,0,0,.7))}\n"] }]
|
|
32250
|
+
}], propDecorators: { canvasSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasSize", required: true }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], svgRef: [{ type: i0.ViewChild, args: ['svg', { isSignal: true }] }] } });
|
|
32251
|
+
|
|
29780
32252
|
/**
|
|
29781
32253
|
* Clamp `index` to the valid range [0, count - 1].
|
|
29782
32254
|
* Returns 0 when `count` is 0 to avoid -1 states.
|
|
@@ -29842,6 +32314,235 @@ function fitZoom(canvasW, canvasH, vw, vh) {
|
|
|
29842
32314
|
return Math.min(vw / canvasW, vh / canvasH);
|
|
29843
32315
|
}
|
|
29844
32316
|
|
|
32317
|
+
/**
|
|
32318
|
+
* presentation-subtitle-helpers.ts — Pure helpers for the subtitle/caption bar.
|
|
32319
|
+
*
|
|
32320
|
+
* Isolates all text-segment logic so it can be unit-tested without DOM or
|
|
32321
|
+
* Angular dependencies.
|
|
32322
|
+
*
|
|
32323
|
+
* Ported from React:
|
|
32324
|
+
* packages/react/src/viewer/components/PresentationSubtitleBar.tsx
|
|
32325
|
+
*/
|
|
32326
|
+
// ---------------------------------------------------------------------------
|
|
32327
|
+
// Caption text merging
|
|
32328
|
+
// ---------------------------------------------------------------------------
|
|
32329
|
+
/**
|
|
32330
|
+
* Merge the results from a `SpeechRecognitionEventLite` (starting at
|
|
32331
|
+
* `resultIndex`) into a single caption string.
|
|
32332
|
+
*
|
|
32333
|
+
* Final results form the stable prefix; interim results form the unstable
|
|
32334
|
+
* suffix. Both are joined and the combined string is trimmed.
|
|
32335
|
+
*
|
|
32336
|
+
* @param event The recognition event from `onresult`.
|
|
32337
|
+
* @param resultIndex `event.resultIndex` — the first new result index.
|
|
32338
|
+
* @param results `event.results` — the full results list.
|
|
32339
|
+
* @returns The merged caption string, or `''` if nothing recognised.
|
|
32340
|
+
*/
|
|
32341
|
+
function mergeCaptionResults(resultIndex, results) {
|
|
32342
|
+
let finalText = '';
|
|
32343
|
+
let interimText = '';
|
|
32344
|
+
for (let i = resultIndex; i < results.length; i += 1) {
|
|
32345
|
+
const result = results[i];
|
|
32346
|
+
const fragment = result?.[0]?.transcript ?? '';
|
|
32347
|
+
if (result?.isFinal) {
|
|
32348
|
+
finalText += fragment;
|
|
32349
|
+
}
|
|
32350
|
+
else {
|
|
32351
|
+
interimText += fragment;
|
|
32352
|
+
}
|
|
32353
|
+
}
|
|
32354
|
+
const merged = `${finalText} ${interimText}`.trim();
|
|
32355
|
+
return merged;
|
|
32356
|
+
}
|
|
32357
|
+
// ---------------------------------------------------------------------------
|
|
32358
|
+
// Safe speech recognition factory
|
|
32359
|
+
// ---------------------------------------------------------------------------
|
|
32360
|
+
/**
|
|
32361
|
+
* Attempt to obtain the `SpeechRecognition` constructor from `globalThis`
|
|
32362
|
+
* (browser `window`). Returns `null` when unavailable (SSR / unsupported
|
|
32363
|
+
* browsers / Firefox without the flag enabled).
|
|
32364
|
+
*
|
|
32365
|
+
* Checks both the standard and the webkit-prefixed name.
|
|
32366
|
+
*/
|
|
32367
|
+
function getSpeechRecognitionCtor() {
|
|
32368
|
+
// Use `globalThis` rather than `window` so this runs in non-DOM environments.
|
|
32369
|
+
const g = globalThis;
|
|
32370
|
+
const ctor = (g['SpeechRecognition'] ?? g['webkitSpeechRecognition']);
|
|
32371
|
+
return ctor ?? null;
|
|
32372
|
+
}
|
|
32373
|
+
/**
|
|
32374
|
+
* Compute the text to display in the caption bar.
|
|
32375
|
+
*
|
|
32376
|
+
* - When the API is unsupported, returns `fallbackNotSupported`.
|
|
32377
|
+
* - When supported but no text has been captured yet, returns `fallbackListening`.
|
|
32378
|
+
* - Otherwise returns the captured text.
|
|
32379
|
+
*/
|
|
32380
|
+
function captionDisplayText(supportState, captionText, fallbackNotSupported, fallbackListening) {
|
|
32381
|
+
if (supportState === 'unsupported') {
|
|
32382
|
+
return fallbackNotSupported;
|
|
32383
|
+
}
|
|
32384
|
+
return captionText.length > 0 ? captionText : fallbackListening;
|
|
32385
|
+
}
|
|
32386
|
+
|
|
32387
|
+
/**
|
|
32388
|
+
* presentation-subtitle-bar.component.ts — Live caption/subtitle bar for
|
|
32389
|
+
* presentation mode, driven by the Web Speech API when available.
|
|
32390
|
+
*
|
|
32391
|
+
* Ported from React:
|
|
32392
|
+
* packages/react/src/viewer/components/PresentationSubtitleBar.tsx
|
|
32393
|
+
*
|
|
32394
|
+
* Selector: `pptx-presentation-subtitle-bar`
|
|
32395
|
+
*
|
|
32396
|
+
* Inputs:
|
|
32397
|
+
* - `visible` (required) — show/hide the bar and start/stop recognition.
|
|
32398
|
+
*
|
|
32399
|
+
* When `visible` is false the component renders nothing and stops the
|
|
32400
|
+
* recognition session. When the Web Speech API is unavailable the bar still
|
|
32401
|
+
* renders but shows a "not supported" message.
|
|
32402
|
+
*
|
|
32403
|
+
* All speech-API access is isolated behind `_createRecognition()` so the rest
|
|
32404
|
+
* of the component (and tests) can stay pure. Caption text accumulation logic
|
|
32405
|
+
* lives in {@link mergeCaptionResults} (pure helper, unit-tested separately).
|
|
32406
|
+
*/
|
|
32407
|
+
class PresentationSubtitleBarComponent {
|
|
32408
|
+
// ------------------------------------------------------------------
|
|
32409
|
+
// Inputs
|
|
32410
|
+
// ------------------------------------------------------------------
|
|
32411
|
+
/** Show the subtitle bar and start speech recognition when true. */
|
|
32412
|
+
visible = input.required(/* @ts-ignore */
|
|
32413
|
+
...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
32414
|
+
// ------------------------------------------------------------------
|
|
32415
|
+
// Internal signals
|
|
32416
|
+
// ------------------------------------------------------------------
|
|
32417
|
+
_captionText = signal('', /* @ts-ignore */
|
|
32418
|
+
...(ngDevMode ? [{ debugName: "_captionText" }] : /* istanbul ignore next */ []));
|
|
32419
|
+
_supportState = signal('unknown', /* @ts-ignore */
|
|
32420
|
+
...(ngDevMode ? [{ debugName: "_supportState" }] : /* istanbul ignore next */ []));
|
|
32421
|
+
/** The text string rendered in the caption bar. */
|
|
32422
|
+
displayText = signal('Listening…', /* @ts-ignore */
|
|
32423
|
+
...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
|
|
32424
|
+
// ------------------------------------------------------------------
|
|
32425
|
+
// Recognition lifecycle bookkeeping
|
|
32426
|
+
// ------------------------------------------------------------------
|
|
32427
|
+
/**
|
|
32428
|
+
* Whether the recognition session should remain running.
|
|
32429
|
+
* Toggled on `visible` changes; checked in `onend` to decide whether
|
|
32430
|
+
* to restart.
|
|
32431
|
+
*/
|
|
32432
|
+
_shouldRun = false;
|
|
32433
|
+
/** Active recognition instance, or null when stopped. */
|
|
32434
|
+
_recognition = null;
|
|
32435
|
+
_destroyRef = inject(DestroyRef);
|
|
32436
|
+
constructor() {
|
|
32437
|
+
this._destroyRef.onDestroy(() => {
|
|
32438
|
+
this._stopRecognition();
|
|
32439
|
+
});
|
|
32440
|
+
}
|
|
32441
|
+
// ------------------------------------------------------------------
|
|
32442
|
+
// Lifecycle
|
|
32443
|
+
// ------------------------------------------------------------------
|
|
32444
|
+
ngOnChanges(changes) {
|
|
32445
|
+
if (!('visible' in changes)) {
|
|
32446
|
+
return;
|
|
32447
|
+
}
|
|
32448
|
+
if (this.visible()) {
|
|
32449
|
+
this._startRecognition();
|
|
32450
|
+
}
|
|
32451
|
+
else {
|
|
32452
|
+
this._stopRecognition();
|
|
32453
|
+
}
|
|
32454
|
+
}
|
|
32455
|
+
// ------------------------------------------------------------------
|
|
32456
|
+
// Speech recognition management
|
|
32457
|
+
// ------------------------------------------------------------------
|
|
32458
|
+
_startRecognition() {
|
|
32459
|
+
this._shouldRun = true;
|
|
32460
|
+
this._captionText.set('');
|
|
32461
|
+
this._updateDisplayText();
|
|
32462
|
+
const Ctor = this._getSpeechCtor();
|
|
32463
|
+
if (!Ctor) {
|
|
32464
|
+
this._supportState.set('unsupported');
|
|
32465
|
+
this._updateDisplayText();
|
|
32466
|
+
return;
|
|
32467
|
+
}
|
|
32468
|
+
this._supportState.set('supported');
|
|
32469
|
+
const recognition = new Ctor();
|
|
32470
|
+
recognition.continuous = true;
|
|
32471
|
+
recognition.interimResults = true;
|
|
32472
|
+
recognition.lang = typeof navigator !== 'undefined' ? navigator.language || 'en-US' : 'en-US';
|
|
32473
|
+
recognition.onresult = (event) => {
|
|
32474
|
+
const merged = mergeCaptionResults(event.resultIndex, event.results);
|
|
32475
|
+
if (merged.length > 0) {
|
|
32476
|
+
this._captionText.set(merged);
|
|
32477
|
+
this._updateDisplayText();
|
|
32478
|
+
}
|
|
32479
|
+
};
|
|
32480
|
+
recognition.onerror = () => {
|
|
32481
|
+
// Keep the bar alive; `onend` will attempt restart if still visible.
|
|
32482
|
+
};
|
|
32483
|
+
recognition.onend = () => {
|
|
32484
|
+
if (!this._shouldRun) {
|
|
32485
|
+
return;
|
|
32486
|
+
}
|
|
32487
|
+
try {
|
|
32488
|
+
recognition.start();
|
|
32489
|
+
}
|
|
32490
|
+
catch {
|
|
32491
|
+
// Browser may throttle rapid restarts; the next visibility toggle will retry.
|
|
32492
|
+
}
|
|
32493
|
+
};
|
|
32494
|
+
this._recognition = recognition;
|
|
32495
|
+
try {
|
|
32496
|
+
recognition.start();
|
|
32497
|
+
}
|
|
32498
|
+
catch {
|
|
32499
|
+
this._supportState.set('unsupported');
|
|
32500
|
+
this._updateDisplayText();
|
|
32501
|
+
}
|
|
32502
|
+
}
|
|
32503
|
+
_stopRecognition() {
|
|
32504
|
+
this._shouldRun = false;
|
|
32505
|
+
if (this._recognition) {
|
|
32506
|
+
this._recognition.stop();
|
|
32507
|
+
this._recognition = null;
|
|
32508
|
+
}
|
|
32509
|
+
this._captionText.set('');
|
|
32510
|
+
this._updateDisplayText();
|
|
32511
|
+
}
|
|
32512
|
+
/**
|
|
32513
|
+
* Thin wrapper around {@link getSpeechRecognitionCtor} so tests can spy on
|
|
32514
|
+
* or override this method without patching `globalThis`.
|
|
32515
|
+
*/
|
|
32516
|
+
_getSpeechCtor() {
|
|
32517
|
+
return getSpeechRecognitionCtor();
|
|
32518
|
+
}
|
|
32519
|
+
// ------------------------------------------------------------------
|
|
32520
|
+
// Display text derivation
|
|
32521
|
+
// ------------------------------------------------------------------
|
|
32522
|
+
_updateDisplayText() {
|
|
32523
|
+
const text = captionDisplayText(this._supportState(), this._captionText(), 'Subtitles not supported in this browser.', 'Listening…');
|
|
32524
|
+
this.displayText.set(text);
|
|
32525
|
+
}
|
|
32526
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationSubtitleBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
32527
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: PresentationSubtitleBarComponent, isStandalone: true, selector: "pptx-presentation-subtitle-bar", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: true, transformFunction: null } }, usesOnChanges: true, ngImport: i0, template: `
|
|
32528
|
+
@if (visible()) {
|
|
32529
|
+
<div class="pptx-ng-subtitle-inner">
|
|
32530
|
+
<span class="pptx-ng-subtitle-text">{{ displayText() }}</span>
|
|
32531
|
+
</div>
|
|
32532
|
+
}
|
|
32533
|
+
`, isInline: true, styles: [":host{display:block;position:absolute;bottom:3.5rem;left:50%;transform:translate(-50%);z-index:70;max-width:80%;min-width:300px;pointer-events:none}.pptx-ng-subtitle-inner{padding:.75rem 1.5rem;border-radius:.5rem;background:#000000bf;border:1px solid rgba(255,255,255,.1);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.pptx-ng-subtitle-text{display:block;text-align:center;font-size:.9375rem;line-height:1.5;color:#ffffffb3;font-style:italic;font-family:system-ui,sans-serif;white-space:pre-wrap;word-break:break-word}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
32534
|
+
}
|
|
32535
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationSubtitleBarComponent, decorators: [{
|
|
32536
|
+
type: Component,
|
|
32537
|
+
args: [{ selector: 'pptx-presentation-subtitle-bar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
32538
|
+
@if (visible()) {
|
|
32539
|
+
<div class="pptx-ng-subtitle-inner">
|
|
32540
|
+
<span class="pptx-ng-subtitle-text">{{ displayText() }}</span>
|
|
32541
|
+
</div>
|
|
32542
|
+
}
|
|
32543
|
+
`, styles: [":host{display:block;position:absolute;bottom:3.5rem;left:50%;transform:translate(-50%);z-index:70;max-width:80%;min-width:300px;pointer-events:none}.pptx-ng-subtitle-inner{padding:.75rem 1.5rem;border-radius:.5rem;background:#000000bf;border:1px solid rgba(255,255,255,.1);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.pptx-ng-subtitle-text{display:block;text-align:center;font-size:.9375rem;line-height:1.5;color:#ffffffb3;font-style:italic;font-family:system-ui,sans-serif;white-space:pre-wrap;word-break:break-word}\n"] }]
|
|
32544
|
+
}], ctorParameters: () => [], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: true }] }] } });
|
|
32545
|
+
|
|
29845
32546
|
// ---------------------------------------------------------------------------
|
|
29846
32547
|
// Direction resolvers
|
|
29847
32548
|
// ---------------------------------------------------------------------------
|
|
@@ -30446,6 +33147,30 @@ function getSlideTransitionAnimations(type, durationMs, direction, orient, spoke
|
|
|
30446
33147
|
const randomType = RANDOM_ELIGIBLE_TYPES[Math.floor(Math.random() * RANDOM_ELIGIBLE_TYPES.length)];
|
|
30447
33148
|
return getSlideTransitionAnimations(randomType, durationMs, direction, orient, spokes);
|
|
30448
33149
|
}
|
|
33150
|
+
// ── Exotic / 3-D transitions — approximated with the closest 2-D CSS
|
|
33151
|
+
// behaviour (mirrors React's CSS fallback for the dynamic-content family
|
|
33152
|
+
// that has no faithful pure-CSS analogue). Delegates to an existing
|
|
33153
|
+
// well-tested case rather than emitting a plain fade.
|
|
33154
|
+
case 'pan':
|
|
33155
|
+
case 'gallery':
|
|
33156
|
+
case 'conveyor':
|
|
33157
|
+
return getSlideTransitionAnimations('push', durationMs, direction, orient, spokes);
|
|
33158
|
+
case 'reveal':
|
|
33159
|
+
return getSlideTransitionAnimations('wipe', durationMs, direction, orient, spokes);
|
|
33160
|
+
case 'doors':
|
|
33161
|
+
return getSlideTransitionAnimations('split', durationMs, direction, orient, spokes);
|
|
33162
|
+
case 'switch':
|
|
33163
|
+
case 'flythrough':
|
|
33164
|
+
case 'ferris':
|
|
33165
|
+
case 'prism':
|
|
33166
|
+
return getSlideTransitionAnimations('zoom', durationMs, direction, orient, spokes);
|
|
33167
|
+
case 'ripple':
|
|
33168
|
+
case 'honeycomb':
|
|
33169
|
+
case 'glitter':
|
|
33170
|
+
case 'shred':
|
|
33171
|
+
return getSlideTransitionAnimations('dissolve', durationMs, direction, orient, spokes);
|
|
33172
|
+
case 'flash':
|
|
33173
|
+
return getSlideTransitionAnimations('fade', durationMs, direction, orient, spokes);
|
|
30449
33174
|
// Fallback (unknown / not-yet-mapped type)
|
|
30450
33175
|
default:
|
|
30451
33176
|
return {
|
|
@@ -30732,6 +33457,11 @@ class PresentationOverlayComponent {
|
|
|
30732
33457
|
...(ngDevMode ? [{ debugName: "activeTransition" }] : /* istanbul ignore next */ []));
|
|
30733
33458
|
/** Click-stepped element-animation playback for the current slide. */
|
|
30734
33459
|
playback = inject(AnimationPlaybackService);
|
|
33460
|
+
/** Ink-annotation state (pen/highlighter/eraser/laser) for the show. */
|
|
33461
|
+
annotations = inject(PresentationAnnotationsService);
|
|
33462
|
+
/** Whether the live-caption bar is shown. */
|
|
33463
|
+
subtitlesVisible = signal(false, /* @ts-ignore */
|
|
33464
|
+
...(ngDevMode ? [{ debugName: "subtitlesVisible" }] : /* istanbul ignore next */ []));
|
|
30735
33465
|
/** The slide stage root — animation styles are applied to its elements. */
|
|
30736
33466
|
stageRef = viewChild('stage', /* @ts-ignore */
|
|
30737
33467
|
...(ngDevMode ? [{ debugName: "stageRef" }] : /* istanbul ignore next */ []));
|
|
@@ -30973,8 +33703,20 @@ class PresentationOverlayComponent {
|
|
|
30973
33703
|
if (event.button !== 0) {
|
|
30974
33704
|
return;
|
|
30975
33705
|
}
|
|
33706
|
+
// A drawing tool owns pointer gestures — don't hijack them to advance.
|
|
33707
|
+
if (this.annotations.tool() !== 'none') {
|
|
33708
|
+
return;
|
|
33709
|
+
}
|
|
30976
33710
|
this.navigate('next');
|
|
30977
33711
|
}
|
|
33712
|
+
/** Toggle an annotation tool (clicking the active one disarms it). */
|
|
33713
|
+
selectTool(tool) {
|
|
33714
|
+
this.annotations.setTool(tool);
|
|
33715
|
+
}
|
|
33716
|
+
/** Toggle the live-caption (subtitle) bar. */
|
|
33717
|
+
toggleSubtitles() {
|
|
33718
|
+
this.subtitlesVisible.update((v) => !v);
|
|
33719
|
+
}
|
|
30978
33720
|
/** Close button click — stop propagation so it does not also advance. */
|
|
30979
33721
|
onClose(event) {
|
|
30980
33722
|
event.stopPropagation();
|
|
@@ -31098,6 +33840,7 @@ class PresentationOverlayComponent {
|
|
|
31098
33840
|
this.activeTransition.set(null);
|
|
31099
33841
|
}
|
|
31100
33842
|
this.currentIndex.set(next);
|
|
33843
|
+
this.annotations.setActiveSlide(next);
|
|
31101
33844
|
this.indexChange.emit(next);
|
|
31102
33845
|
}
|
|
31103
33846
|
}
|
|
@@ -31105,7 +33848,7 @@ class PresentationOverlayComponent {
|
|
|
31105
33848
|
this.closed.emit();
|
|
31106
33849
|
}
|
|
31107
33850
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
31108
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: PresentationOverlayComponent, isStandalone: true, selector: "pptx-presentation-overlay", inputs: { slides: { classPropertyName: "slides", publicName: "slides", isSignal: true, isRequired: true, transformFunction: null }, canvasSize: { classPropertyName: "canvasSize", publicName: "canvasSize", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, startIndex: { classPropertyName: "startIndex", publicName: "startIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { indexChange: "indexChange", closed: "closed" }, host: { listeners: { "window:resize": "onWindowResize()", "document:keydown": "onKeyDown($event)" } }, providers: [AnimationPlaybackService], viewQueries: [{ propertyName: "stageRef", first: true, predicate: ["stage"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
33851
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: PresentationOverlayComponent, isStandalone: true, selector: "pptx-presentation-overlay", inputs: { slides: { classPropertyName: "slides", publicName: "slides", isSignal: true, isRequired: true, transformFunction: null }, canvasSize: { classPropertyName: "canvasSize", publicName: "canvasSize", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, startIndex: { classPropertyName: "startIndex", publicName: "startIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { indexChange: "indexChange", closed: "closed" }, host: { listeners: { "window:resize": "onWindowResize()", "document:keydown": "onKeyDown($event)" } }, providers: [AnimationPlaybackService, PresentationAnnotationsService], viewQueries: [{ propertyName: "stageRef", first: true, predicate: ["stage"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
31109
33852
|
<div
|
|
31110
33853
|
class="pptx-ng-presentation-root"
|
|
31111
33854
|
(touchstart)="onTouchStart($event)"
|
|
@@ -31134,6 +33877,59 @@ class PresentationOverlayComponent {
|
|
|
31134
33877
|
(complete)="activeTransition.set(null)"
|
|
31135
33878
|
/>
|
|
31136
33879
|
}
|
|
33880
|
+
|
|
33881
|
+
<!-- Ink annotation overlay (pen/highlighter/eraser/laser). -->
|
|
33882
|
+
<pptx-presentation-annotation-overlay [canvasSize]="canvasSize()" [zoom]="zoom()" />
|
|
33883
|
+
</div>
|
|
33884
|
+
|
|
33885
|
+
<!-- Live-caption (subtitle) bar. -->
|
|
33886
|
+
<pptx-presentation-subtitle-bar [visible]="subtitlesVisible()" />
|
|
33887
|
+
|
|
33888
|
+
<!-- Annotation tool toolbar (bottom-centre). -->
|
|
33889
|
+
<div class="pptx-ng-presentation-tools" role="toolbar" aria-label="Annotation tools">
|
|
33890
|
+
<button
|
|
33891
|
+
type="button"
|
|
33892
|
+
[class.is-active]="annotations.tool() === 'pen'"
|
|
33893
|
+
(click)="selectTool('pen')"
|
|
33894
|
+
aria-label="Pen"
|
|
33895
|
+
>
|
|
33896
|
+
✎
|
|
33897
|
+
</button>
|
|
33898
|
+
<button
|
|
33899
|
+
type="button"
|
|
33900
|
+
[class.is-active]="annotations.tool() === 'highlighter'"
|
|
33901
|
+
(click)="selectTool('highlighter')"
|
|
33902
|
+
aria-label="Highlighter"
|
|
33903
|
+
>
|
|
33904
|
+
▭
|
|
33905
|
+
</button>
|
|
33906
|
+
<button
|
|
33907
|
+
type="button"
|
|
33908
|
+
[class.is-active]="annotations.tool() === 'eraser'"
|
|
33909
|
+
(click)="selectTool('eraser')"
|
|
33910
|
+
aria-label="Eraser"
|
|
33911
|
+
>
|
|
33912
|
+
⌫
|
|
33913
|
+
</button>
|
|
33914
|
+
<button
|
|
33915
|
+
type="button"
|
|
33916
|
+
[class.is-active]="annotations.tool() === 'laser'"
|
|
33917
|
+
(click)="selectTool('laser')"
|
|
33918
|
+
aria-label="Laser pointer"
|
|
33919
|
+
>
|
|
33920
|
+
•
|
|
33921
|
+
</button>
|
|
33922
|
+
<button type="button" (click)="annotations.clearAnnotations()" aria-label="Clear ink">
|
|
33923
|
+
🗑
|
|
33924
|
+
</button>
|
|
33925
|
+
<button
|
|
33926
|
+
type="button"
|
|
33927
|
+
[class.is-active]="subtitlesVisible()"
|
|
33928
|
+
(click)="toggleSubtitles()"
|
|
33929
|
+
aria-label="Live captions"
|
|
33930
|
+
>
|
|
33931
|
+
CC
|
|
33932
|
+
</button>
|
|
31137
33933
|
</div>
|
|
31138
33934
|
|
|
31139
33935
|
<!-- Always-visible close button (top-right, safe-area aware). -->
|
|
@@ -31175,11 +33971,17 @@ class PresentationOverlayComponent {
|
|
|
31175
33971
|
{{ counterLabel() }}
|
|
31176
33972
|
</span>
|
|
31177
33973
|
</div>
|
|
31178
|
-
`, isInline: true, styles: [":host{display:block;position:fixed;inset:0;z-index:10000;background:#000;cursor:pointer;-webkit-user-select:none;user-select:none}.pptx-ng-presentation-root{position:absolute;inset:0;touch-action:pan-y}.pptx-ng-presentation-close:hover,.pptx-ng-presentation-nav:hover{background:#000000bf}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SlideCanvasComponent, selector: "pptx-slide-canvas", inputs: ["slide", "canvasSize", "mediaDataUrls", "zoom", "editable", "selectedIds", "editingId"], outputs: ["elementSelect", "backgroundClick", "transformStart", "transformUpdate", "contextMenu", "textEditStart", "textCommit", "textCancel", "rotateUpdate", "marqueeSelect"] }, { kind: "component", type: PresentationTransitionOverlayComponent, selector: "pptx-presentation-transition-overlay", inputs: ["outgoingSlide", "canvasSize", "transition", "templateElements", "mediaDataUrls", "durationMs"], outputs: ["complete"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
33974
|
+
`, isInline: true, styles: [":host{display:block;position:fixed;inset:0;z-index:10000;background:#000;cursor:pointer;-webkit-user-select:none;user-select:none}.pptx-ng-presentation-root{position:absolute;inset:0;touch-action:pan-y}.pptx-ng-presentation-close:hover,.pptx-ng-presentation-nav:hover{background:#000000bf}.pptx-ng-presentation-tools{position:absolute;bottom:max(1rem,env(safe-area-inset-bottom));left:50%;transform:translate(-50%);display:flex;gap:.25rem;padding:.25rem;border-radius:.5rem;background:#0000008c;z-index:80}.pptx-ng-presentation-tools button{width:2rem;height:2rem;border:none;border-radius:.35rem;background:transparent;color:#fff;font-size:1rem;cursor:pointer}.pptx-ng-presentation-tools button:hover{background:#ffffff26}.pptx-ng-presentation-tools button.is-active{background:#ffffff4d}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SlideCanvasComponent, selector: "pptx-slide-canvas", inputs: ["slide", "canvasSize", "mediaDataUrls", "zoom", "editable", "selectedIds", "editingId"], outputs: ["elementSelect", "backgroundClick", "transformStart", "transformUpdate", "contextMenu", "textEditStart", "textCommit", "textCancel", "rotateUpdate", "marqueeSelect"] }, { kind: "component", type: PresentationTransitionOverlayComponent, selector: "pptx-presentation-transition-overlay", inputs: ["outgoingSlide", "canvasSize", "transition", "templateElements", "mediaDataUrls", "durationMs"], outputs: ["complete"] }, { kind: "component", type: PresentationAnnotationOverlayComponent, selector: "pptx-presentation-annotation-overlay", inputs: ["canvasSize", "zoom"] }, { kind: "component", type: PresentationSubtitleBarComponent, selector: "pptx-presentation-subtitle-bar", inputs: ["visible"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
31179
33975
|
}
|
|
31180
33976
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PresentationOverlayComponent, decorators: [{
|
|
31181
33977
|
type: Component,
|
|
31182
|
-
args: [{ selector: 'pptx-presentation-overlay', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
33978
|
+
args: [{ selector: 'pptx-presentation-overlay', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
33979
|
+
NgStyle,
|
|
33980
|
+
SlideCanvasComponent,
|
|
33981
|
+
PresentationTransitionOverlayComponent,
|
|
33982
|
+
PresentationAnnotationOverlayComponent,
|
|
33983
|
+
PresentationSubtitleBarComponent,
|
|
33984
|
+
], providers: [AnimationPlaybackService, PresentationAnnotationsService], template: `
|
|
31183
33985
|
<div
|
|
31184
33986
|
class="pptx-ng-presentation-root"
|
|
31185
33987
|
(touchstart)="onTouchStart($event)"
|
|
@@ -31208,6 +34010,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
31208
34010
|
(complete)="activeTransition.set(null)"
|
|
31209
34011
|
/>
|
|
31210
34012
|
}
|
|
34013
|
+
|
|
34014
|
+
<!-- Ink annotation overlay (pen/highlighter/eraser/laser). -->
|
|
34015
|
+
<pptx-presentation-annotation-overlay [canvasSize]="canvasSize()" [zoom]="zoom()" />
|
|
34016
|
+
</div>
|
|
34017
|
+
|
|
34018
|
+
<!-- Live-caption (subtitle) bar. -->
|
|
34019
|
+
<pptx-presentation-subtitle-bar [visible]="subtitlesVisible()" />
|
|
34020
|
+
|
|
34021
|
+
<!-- Annotation tool toolbar (bottom-centre). -->
|
|
34022
|
+
<div class="pptx-ng-presentation-tools" role="toolbar" aria-label="Annotation tools">
|
|
34023
|
+
<button
|
|
34024
|
+
type="button"
|
|
34025
|
+
[class.is-active]="annotations.tool() === 'pen'"
|
|
34026
|
+
(click)="selectTool('pen')"
|
|
34027
|
+
aria-label="Pen"
|
|
34028
|
+
>
|
|
34029
|
+
✎
|
|
34030
|
+
</button>
|
|
34031
|
+
<button
|
|
34032
|
+
type="button"
|
|
34033
|
+
[class.is-active]="annotations.tool() === 'highlighter'"
|
|
34034
|
+
(click)="selectTool('highlighter')"
|
|
34035
|
+
aria-label="Highlighter"
|
|
34036
|
+
>
|
|
34037
|
+
▭
|
|
34038
|
+
</button>
|
|
34039
|
+
<button
|
|
34040
|
+
type="button"
|
|
34041
|
+
[class.is-active]="annotations.tool() === 'eraser'"
|
|
34042
|
+
(click)="selectTool('eraser')"
|
|
34043
|
+
aria-label="Eraser"
|
|
34044
|
+
>
|
|
34045
|
+
⌫
|
|
34046
|
+
</button>
|
|
34047
|
+
<button
|
|
34048
|
+
type="button"
|
|
34049
|
+
[class.is-active]="annotations.tool() === 'laser'"
|
|
34050
|
+
(click)="selectTool('laser')"
|
|
34051
|
+
aria-label="Laser pointer"
|
|
34052
|
+
>
|
|
34053
|
+
•
|
|
34054
|
+
</button>
|
|
34055
|
+
<button type="button" (click)="annotations.clearAnnotations()" aria-label="Clear ink">
|
|
34056
|
+
🗑
|
|
34057
|
+
</button>
|
|
34058
|
+
<button
|
|
34059
|
+
type="button"
|
|
34060
|
+
[class.is-active]="subtitlesVisible()"
|
|
34061
|
+
(click)="toggleSubtitles()"
|
|
34062
|
+
aria-label="Live captions"
|
|
34063
|
+
>
|
|
34064
|
+
CC
|
|
34065
|
+
</button>
|
|
31211
34066
|
</div>
|
|
31212
34067
|
|
|
31213
34068
|
<!-- Always-visible close button (top-right, safe-area aware). -->
|
|
@@ -31249,7 +34104,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
31249
34104
|
{{ counterLabel() }}
|
|
31250
34105
|
</span>
|
|
31251
34106
|
</div>
|
|
31252
|
-
`, styles: [":host{display:block;position:fixed;inset:0;z-index:10000;background:#000;cursor:pointer;-webkit-user-select:none;user-select:none}.pptx-ng-presentation-root{position:absolute;inset:0;touch-action:pan-y}.pptx-ng-presentation-close:hover,.pptx-ng-presentation-nav:hover{background:#000000bf}\n"] }]
|
|
34107
|
+
`, styles: [":host{display:block;position:fixed;inset:0;z-index:10000;background:#000;cursor:pointer;-webkit-user-select:none;user-select:none}.pptx-ng-presentation-root{position:absolute;inset:0;touch-action:pan-y}.pptx-ng-presentation-close:hover,.pptx-ng-presentation-nav:hover{background:#000000bf}.pptx-ng-presentation-tools{position:absolute;bottom:max(1rem,env(safe-area-inset-bottom));left:50%;transform:translate(-50%);display:flex;gap:.25rem;padding:.25rem;border-radius:.5rem;background:#0000008c;z-index:80}.pptx-ng-presentation-tools button{width:2rem;height:2rem;border:none;border-radius:.35rem;background:transparent;color:#fff;font-size:1rem;cursor:pointer}.pptx-ng-presentation-tools button:hover{background:#ffffff26}.pptx-ng-presentation-tools button.is-active{background:#ffffff4d}\n"] }]
|
|
31253
34108
|
}], ctorParameters: () => [], propDecorators: { slides: [{ type: i0.Input, args: [{ isSignal: true, alias: "slides", required: true }] }], canvasSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "canvasSize", required: true }] }], mediaDataUrls: [{ type: i0.Input, args: [{ isSignal: true, alias: "mediaDataUrls", required: false }] }], startIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "startIndex", required: false }] }], indexChange: [{ type: i0.Output, args: ["indexChange"] }], closed: [{ type: i0.Output, args: ["closed"] }], stageRef: [{ type: i0.ViewChild, args: ['stage', { isSignal: true }] }], onWindowResize: [{
|
|
31254
34109
|
type: HostListener,
|
|
31255
34110
|
args: ['window:resize']
|
|
@@ -36443,5 +39298,5 @@ function cn(...values) {
|
|
|
36443
39298
|
* Generated bundle index. Do not edit.
|
|
36444
39299
|
*/
|
|
36445
39300
|
|
|
36446
|
-
export { AccessibilityPanelComponent, AccessibilityService, AnimationAuthorPanelComponent, AnimationPanelComponent, AnimationPlaybackService, BroadcastDialogComponent, CURSOR_PALETTE, ChartDataEditorComponent, ChartRendererComponent, CollaborationCursorsComponent, CollaborationService, CommentsPanelComponent, CommentsService, ConnectorRendererComponent, ConnectorTextOverlayComponent, DEFAULT_BROADCAST_SERVER_URL, DEFAULT_CANVAS_HEIGHT, DEFAULT_CANVAS_WIDTH, DEFAULT_FILL_COLOR, DEFAULT_PRINT_SETTINGS, DEFAULT_SLIDE_BACKGROUND, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_COLOR$1 as DEFAULT_TEXT_COLOR, EMBEDDED_FONTS_STYLE_ID, EditorContextMenuComponent, EditorHistory, EditorStateService, EditorToolbarComponent, EffectsPanelComponent, ElementRendererComponent, EmbeddedFontsService, EquationRendererComponent, ExportService, FindBarComponent, FindReplaceBarComponent, GradientPickerComponent, HANDOUT_OPTIONS, HyperlinkDialogComponent, InkRendererComponent, InspectorPanelComponent, IsMobileService, LoadContentService, MobileBottomBarComponent, MobileMenuSheetComponent, MobileSheetComponent, MobileSlidesSheetComponent, ModalDialogComponent, Model3DRendererComponent, OleRendererComponent, PowerPointViewerComponent, PresentationOverlayComponent, PresentationTransitionOverlayComponent, PresenterViewComponent, PrintDialogComponent, PrintService, PrintSettingsPanelComponent, PropertiesDialogComponent, RESIZE_HANDLES, SEVERITY_GROUPS, SEVERITY_LABELS, SLIDE_TRANSITION_KEYFRAMES, ShareDialogComponent, SignaturesPanelComponent, SignaturesService, SlideCanvasComponent, SlideSorterOverlayComponent, SlidesPanelComponent, SmartArtRendererComponent, TYPE_LABELS$1 as TYPE_LABELS, TableDataEditorComponent, TableRendererComponent, TextAdvancedPanelComponent, VIEWER_THEME, WEBM_MIME_CANDIDATES, ZoomRendererComponent, addCommentToList, advanceStep, applyFindReplacements, applyMove, applyResize, assignUserColor, bringForward, bringToFront, buildBroadcastConfig, buildBroadcastViewerUrl, buildClearHyperlinkPatch, buildClickGroups, buildCollaborationConfig, buildCssGradientFromShapeStyle, buildDuotoneFilter, buildDuotoneFilterId, buildEmbeddedFontStyles, buildFontFaceRule, buildHyperlinkPatch, buildPatternFillCss, buildPrintDocument, buildPropertiesPatch, buildShareUrl, bulletIndentPx, canStartBroadcast, canStartShare, canUseClipboard, clampCursorPosition, clampGifDimensions, clampNotesFontSize, clampStep, cn, collectAccessibilityIssues, collectElementText, collectSlideText, computeHandoutLayout, computeIsMobile, computeIsTablet, computePageCount, computeSlideIndices, computeTimerProgress, convertOmmlToMathMl, countAccessibilityIssues, defaultCssVars, defaultRadius, defaultThemeColors, deleteElementsByIds, derivePresenceList, duplicateElementById, durationOf, encodeGif, estimatePageCount, findInSlides, fontMimeForFormat, formatAutoNumber, formatCursorLabel, formatElapsed, formatPropertyDate, formatTime, fpsToFrameIntervalMs, generateBroadcastRoomId, generateCommentId, getContainerStyle, getDuotoneFilterDef, getImageSrc, getPatternSvg, getResolvedShapeClipPath, getResolvedShapeClipPathFor, getShapeFillStrokeStyle, getSlideBackgroundStyle, getSlideTransitionAnimations, getTextBlockStyle, groupIssuesBySeverity, hasExistingLink, headerLabel, isInjectableUrl, isPpactionUrl, isSigned, isUrlSafe, isValidRoomId, issueTrackKey, issueTypeLabel, moveElementBy, msToFrameDelayCs, normalizeFontFormat, normalizeSlidesPerPage, ommlToMathml, overallStatus, pendingElementStyles, pickSupportedMimeType, planGifFrames, planVideoSegments, presenceToCursors, provideViewerTheme, recordWebm, removeCommentFromList, renderToCanvas, replaceInSlides, replaceMatch, resizeElement, resolveFontVariant, resolveHyperlinkHref, resolveParagraphBullet, resolvePresenterNotes, resolveTransitionDuration, revealedElementStyles, routeOrthogonalConnector, sanitizeColor, sanitizeSlideIndex, sanitizeUserName, searchSlides, seedBroadcastFields, seedHyperlinkDraft, seedPropertiesDraft, seedShareFields, segmentFrameCount, sendBackward, sendToBack, setElementPosition, signatureCountLabel, signatureKey, signatureTimestamp, signerName, statusKind, statusLabel, themeStyle, themeToCssVars, toggleCommentResolvedInList, updateElementById, validatePrintSettings, validateRoomId, waypointsToPathD, worstStatus };
|
|
39301
|
+
export { AccessibilityPanelComponent, AccessibilityService, AnimationAuthorPanelComponent, AnimationPanelComponent, AnimationPlaybackService, BroadcastDialogComponent, CURSOR_PALETTE, ChartDataEditorComponent, ChartRendererComponent, CollaborationCursorsComponent, CollaborationService, CommentsPanelComponent, CommentsService, ConnectorRendererComponent, ConnectorTextOverlayComponent, DEFAULT_BROADCAST_SERVER_URL, DEFAULT_CANVAS_HEIGHT, DEFAULT_CANVAS_WIDTH, DEFAULT_FILL_COLOR, DEFAULT_PRINT_SETTINGS, DEFAULT_SLIDE_BACKGROUND, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_COLOR$1 as DEFAULT_TEXT_COLOR, EMBEDDED_FONTS_STYLE_ID, EditorContextMenuComponent, EditorHistory, EditorStateService, EditorToolbarComponent, EffectsPanelComponent, ElementRendererComponent, EmbeddedFontsService, EquationRendererComponent, ExportService, FindBarComponent, FindReplaceBarComponent, GradientPickerComponent, HANDOUT_OPTIONS, HyperlinkDialogComponent, InkRendererComponent, InspectorPanelComponent, IsMobileService, LoadContentService, MobileBottomBarComponent, MobileMenuSheetComponent, MobileSheetComponent, MobileSlidesSheetComponent, ModalDialogComponent, Model3DRendererComponent, OleRendererComponent, PowerPointViewerComponent, PresentationAnnotationOverlayComponent, PresentationAnnotationsService, PresentationOverlayComponent, PresentationSubtitleBarComponent, PresentationTransitionOverlayComponent, PresenterViewComponent, PrintDialogComponent, PrintService, PrintSettingsPanelComponent, PropertiesDialogComponent, RESIZE_HANDLES, SEVERITY_GROUPS, SEVERITY_LABELS, SLIDE_TRANSITION_KEYFRAMES, SVG_WARP_PRESETS, ShareDialogComponent, SignaturesPanelComponent, SignaturesService, SlideCanvasComponent, SlideSorterOverlayComponent, SlidesPanelComponent, SmartArtRendererComponent, TYPE_LABELS$1 as TYPE_LABELS, TableDataEditorComponent, TableRendererComponent, TextAdvancedPanelComponent, VIEWER_THEME, WEBM_MIME_CANDIDATES, ZoomRendererComponent, addCommentToList, advanceStep, applyFindReplacements, applyMove, applyResize, assignUserColor, bringForward, bringToFront, buildBroadcastConfig, buildBroadcastViewerUrl, buildClearHyperlinkPatch, buildClickGroups, buildCollaborationConfig, buildCssGradientFromShapeStyle, buildDuotoneFilter, buildDuotoneFilterId, buildEmbeddedFontStyles, buildFontFaceRule, buildHyperlinkPatch, buildPatternFillCss, buildPrintDocument, buildPropertiesPatch, buildShareUrl, bulletIndentPx, canStartBroadcast, canStartShare, canUseClipboard, clampCursorPosition, clampGifDimensions, clampNotesFontSize, clampStep, cn, collectAccessibilityIssues, collectElementText, collectSlideText, computeHandoutLayout, computeIsMobile, computeIsTablet, computePageCount, computeSlideIndices, computeTimerProgress, convertOmmlToMathMl, countAccessibilityIssues, defaultCssVars, defaultRadius, defaultThemeColors, deleteElementsByIds, derivePresenceList, duplicateElementById, durationOf, encodeGif, estimatePageCount, findInSlides, fontMimeForFormat, formatAutoNumber, formatCursorLabel, formatElapsed, formatPropertyDate, formatTime, fpsToFrameIntervalMs, generateBroadcastRoomId, generateCommentId, getContainerStyle, getDuotoneFilterDef, getImageSrc, getPatternSvg, getResolvedShapeClipPath, getResolvedShapeClipPathFor, getShapeFillStrokeStyle, getSlideBackgroundStyle, getSlideTransitionAnimations, getTextBlockStyle, getTextWarp, getWarpCategory, getWarpPath, groupIssuesBySeverity, hasExistingLink, headerLabel, isInjectableUrl, isPpactionUrl, isSigned, isUrlSafe, isValidRoomId, issueTrackKey, issueTypeLabel, moveElementBy, msToFrameDelayCs, normalizeFontFormat, normalizeSlidesPerPage, ommlToMathml, overallStatus, pendingElementStyles, pickSupportedMimeType, planGifFrames, planVideoSegments, presenceToCursors, provideViewerTheme, recordWebm, removeCommentFromList, renderToCanvas, replaceInSlides, replaceMatch, resizeElement, resolveFontVariant, resolveHyperlinkHref, resolveParagraphBullet, resolvePresenterNotes, resolveTransitionDuration, revealedElementStyles, routeOrthogonalConnector, sanitizeColor, sanitizeSlideIndex, sanitizeUserName, searchSlides, seedBroadcastFields, seedHyperlinkDraft, seedPropertiesDraft, seedShareFields, segmentFrameCount, sendBackward, sendToBack, setElementPosition, shouldUseSvgWarp, signatureCountLabel, signatureKey, signatureTimestamp, signerName, statusKind, statusLabel, themeStyle, themeToCssVars, toggleCommentResolvedInList, updateElementById, validatePrintSettings, validateRoomId, waypointsToPathD, worstStatus };
|
|
36447
39302
|
//# sourceMappingURL=pptx-angular-viewer.mjs.map
|