jvs-ppt-ui 0.0.15 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/lib/jvs-ppt-ui.es.js +20301 -19823
  2. package/dist/lib/jvs-ppt-ui.umd.js +99 -99
  3. package/dist/lib/style.css +1 -1
  4. package/package.json +1 -1
  5. package/src/components/CanvasArea.vue +14 -115
  6. package/src/components/ChartTypeSelector.vue +34 -30
  7. package/src/components/Header.vue +40 -5
  8. package/src/components/PresentationGroupChild.vue +44 -13
  9. package/src/components/PresentationMode.vue +84 -33
  10. package/src/components/SidebarLeft.vue +96 -18
  11. package/src/components/SidebarRight.vue +6 -3
  12. package/src/components/Toolbar.vue +1 -1
  13. package/src/components/canvas/elements/CanvasShapeElement.vue +91 -34
  14. package/src/components/sidebar/common/SidebarBorderAccordion.vue +2 -2
  15. package/src/components/sidebar/common/SidebarReflectionAccordion.vue +2 -2
  16. package/src/components/sidebar/common/SidebarShadowAccordion.vue +2 -2
  17. package/src/components/sidebar/panels/SidebarChartFormatPanel.vue +3 -3
  18. package/src/components/sidebar/panels/SidebarDesignPanel.vue +170 -9
  19. package/src/components/sidebar/panels/SidebarShapeFormatPanel.vue +176 -16
  20. package/src/components/sidebar/panels/SidebarTableFormatPanel.vue +2 -2
  21. package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +2 -2
  22. package/src/components/sidebar/panels/SidebarTransitionPanel.vue +30 -19
  23. package/src/composables/canvas/useCanvasTransform.ts +24 -0
  24. package/src/composables/sidebar/useSidebarShapeFormat.ts +248 -17
  25. package/src/composables/useSlides.ts +12 -0
  26. package/src/style.css +4 -2
  27. package/src/utils/canvas/elementStyles.ts +22 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jvs-ppt-ui",
3
3
  "private": false,
4
- "version": "0.0.15",
4
+ "version": "0.0.16",
5
5
  "type": "module",
6
6
  "main": "./dist/lib/jvs-ppt-ui.umd.js",
7
7
  "module": "./dist/lib/jvs-ppt-ui.es.js",
@@ -5197,8 +5197,8 @@ const selectedElementKeypoints = computed(() => {
5197
5197
  const actualH = geom.height || 200
5198
5198
  const viewBoxW = el.viewBox?.[0] || 200
5199
5199
  const viewBoxH = el.viewBox?.[1] || 200
5200
- const w = viewBoxW
5201
- const h = viewBoxH
5200
+ const w = el.pathFormula === 'roundRect' ? actualW : viewBoxW
5201
+ const h = el.pathFormula === 'roundRect' ? actualH : viewBoxH
5202
5202
 
5203
5203
  return el.keypoints.map((keypoint: any, index: number) => {
5204
5204
  const getBaseSize = formulaObj.getBaseSize?.[index]
@@ -8154,7 +8154,7 @@ async function handleGlobalPaste(e: ClipboardEvent) {
8154
8154
  : (el.type === 'line'
8155
8155
  ? `${el.style?.left}_${el.style?.top}_${el.style?.width}_${el.style?.height}_${el.start?.join(',')}_${el.end?.join(',')}_${el.broken?.join(',')}_${el.broken2?.join(',')}_${el.curve?.join(',')}`
8156
8156
  : 'frozen'))
8157
- : `${el.id}_${el.style?.left}_${el.style?.top}_${el.style?.width}_${el.style?.height}_${el.style?.rotate}_${el.type === 'line' ? `${el.start?.join(',')}_${el.end?.join(',')}_${el.broken?.join(',')}_${el.broken2?.join(',')}_${el.curve?.join(',')}_` : ''}${selectedElementIds.includes(el.id)}_${selectedElementId === el.id}_${isEditingText}_${activeTab}_${activeSlideVersion}`
8157
+ : `${el.id}_${el.style?.left}_${el.style?.top}_${el.style?.width}_${el.style?.height}_${el.style?.rotate}_${el.type === 'line' ? `${el.start?.join(',')}_${el.end?.join(',')}_${el.broken?.join(',')}_${el.broken2?.join(',')}_${el.curve?.join(',')}_` : ''}${selectedElementIds.includes(el.id)}_${selectedElementId === el.id}_${isEditingText}_${activeTab}_${activeSlideVersion}_${(el.style as any)?.reflection?.enabled}_${(el.style as any)?.reflection?.distance}_${(el.style as any)?.reflection?.size}_${(el.style as any)?.reflection?.blur}_${(el.style as any)?.reflection?.opacity}`
8158
8158
  ]"
8159
8159
  :class="{
8160
8160
  selected: selectedElementIds.includes(el.id),
@@ -8428,118 +8428,17 @@ async function handleGlobalPaste(e: ClipboardEvent) {
8428
8428
 
8429
8429
  <!-- Shape element -->
8430
8430
  <template v-else-if="el.type === 'shape'">
8431
- <div v-if="el.content === 'circle'" :style="{
8432
- width: '100%',
8433
- height: '100%',
8434
- backgroundColor: el.style.fill !== undefined ? el.style.fill : '#3b82f6',
8435
- borderRadius: '50%',
8436
- borderWidth: el.style.borderWidth || '2px',
8437
- borderStyle: el.style.borderStyle || 'solid',
8438
- borderColor: el.style.borderColor || '#2563eb',
8439
- filter: (el.shadow && el.shadow.type === 'inner') ? `url(#shape-inner-shadow-${el.id})` : ''
8440
- }"></div>
8441
- <div v-else-if="el.content === 'triangle'" style="width: 100%; height: 100%; overflow: visible;">
8442
- <svg viewBox="0 0 100 100" preserveAspectRatio="none"
8443
- style="width: 100%; height: 100%; display: block; overflow: visible;"
8444
- :style="{ filter: (el.shadow && el.shadow.type === 'inner') ? `url(#shape-inner-shadow-${el.id})` : '' }">
8445
- <polygon points="50,0 0,100 100,100"
8446
- :fill="el.style.fill !== undefined ? el.style.fill : '#f59e0b'"
8447
- :stroke="el.style.borderColor || '#d97706'"
8448
- :stroke-width="el.style.borderWidth ? parseInt(el.style.borderWidth) * 1.5 : 3"
8449
- :stroke-dasharray="el.style.borderStyle === 'dashed' ? '5 5' : ''" />
8450
- </svg>
8451
- </div>
8452
- <div v-else-if="el.content && (el.content.trim().startsWith('M') || el.content.trim().startsWith('m'))"
8453
- style="width: 100%; height: 100%; overflow: visible;">
8454
- <svg overflow="visible" style="width: 100%; height: 100%; display: block; overflow: visible;"
8455
- :style="{ filter: (el.shadow && el.shadow.type === 'inner') ? `url(#shape-inner-shadow-${el.id})` : '' }">
8456
- <g
8457
- :transform="el.shapType === 'arc' ? `scale(${getElementWidthHeight(el).width / (el.viewBox ? el.viewBox[0] : 200)}, ${getElementWidthHeight(el).height / (el.viewBox ? el.viewBox[1] : 200)})` : `translate(${(el.style.borderWidth ? parseFloat(el.style.borderWidth) : 2) / 2}, ${(el.style.borderWidth ? parseFloat(el.style.borderWidth) : 2) / 2}) scale(${Math.max(0, getElementWidthHeight(el).width - (el.style.borderWidth ? parseFloat(el.style.borderWidth) : 2)) / (el.viewBox ? el.viewBox[0] : 200)}, ${Math.max(0, getElementWidthHeight(el).height - (el.style.borderWidth ? parseFloat(el.style.borderWidth) : 2)) / (el.viewBox ? el.viewBox[1] : 200)})`">
8458
- <path vector-effect="non-scaling-stroke" :d="el.content"
8459
- :fill="getElementFill(el) !== undefined ? getElementFill(el) : '#3b82f6'"
8460
- :stroke="getElementBorderColor(el) || 'none'" :stroke-width="getElementBorderWidth(el)"
8461
- :stroke-linecap="(el.style?.strokeLinecap || el.borderLineCap || el.lineCap || (el.shapType === 'arc' ? 'round' : undefined)) as any"
8462
- :stroke-dasharray="el.style?.borderStyle === 'dashed' || (el as any).borderType === 'dashed' ? '5 5' : ''" />
8463
- </g>
8464
- </svg>
8465
- </div>
8466
- <div v-else :style="{
8467
- width: '100%',
8468
- height: '100%',
8469
- backgroundColor: (getElementFill(el) !== undefined && getElementFill(el) !== 'none') ? getElementFill(el) : (getElementFill(el) === 'none' ? 'transparent' : '#10b981'),
8470
- borderRadius: '4px',
8471
- borderWidth: `${getElementBorderWidth(el)}px`,
8472
- borderStyle: el.style?.borderStyle || (el as any).borderType || 'solid',
8473
- borderColor: getElementBorderColor(el) || 'transparent',
8474
- filter: (el.shadow && el.shadow.type === 'inner') ? `url(#shape-inner-shadow-${el.id})` : ''
8475
- }"></div>
8476
-
8477
- <div class="shape-text-overlay-wrapper"
8478
- style="position: absolute; display: flex; flex-direction: column; pointer-events: none; box-sizing: border-box;"
8479
- :style="{
8480
- left: isEllipseShape(el) ? '14.6%' : '0px',
8481
- top: isEllipseShape(el) ? '14.6%' : '0px',
8482
- width: isEllipseShape(el) ? '70.8%' : '100%',
8483
- height: isEllipseShape(el) ? '70.8%' : '100%',
8484
- justifyContent: (el.style.verticalAlign === 'top' || el.style.verticalAlign === 'up') ? 'flex-start' : ((el.style.verticalAlign === 'bottom' || el.style.verticalAlign === 'down') ? 'flex-end' : 'center'),
8485
- padding: isEllipseShape(el) ? '0px' : (el.style.padding || '4.8px 9.6px 4.8px 9.6px')
8486
- }">
8487
- <div class="text-reflection-wrapper" style="position: relative; width: 100%; height: auto;">
8488
- <div class="shape-text-overlay" :style="{
8489
- display: 'block',
8490
- width: '100%',
8491
- maxWidth: '100%',
8492
- outline: 'none',
8493
- padding: '0px',
8494
- boxSizing: 'content-box',
8495
- overflow: 'wrap',
8496
- wordBreak: 'break-word',
8497
- pointerEvents: 'none',
8498
- userSelect: 'none',
8499
- color: el.style.textColor || el.style.color || (el.style.fill === '#ffffff' || el.style.fill === '#FFFFFF' ? '#475569' : '#ffffff'),
8500
- fontFamily: el.style.fontFamily || 'inherit',
8501
- fontSize: el.style.fontSize || '16px',
8502
- fontWeight: el.style.fontWeight || ((getSafeShapeText(el).includes('<')) ? 'normal' : 'normal'),
8503
- fontStyle: (el.style as any).fontStyle || 'normal',
8504
- textDecoration: (el.style.textDecoration as any) || 'none',
8505
- lineHeight: el.style.lineHeight || 1.35,
8506
- textAlign: (el.style.textAlign as any) || 'center',
8507
- whiteSpace: (el.style as any).whiteSpace || 'normal',
8508
- '--paragraphSpace': `${(el.style as any).paragraphSpace === undefined ? 3 : (el.style as any).paragraphSpace}px`
8509
- }" v-html="normalizeHtmlFontFamily(getSafeShapeText(el))">
8510
- </div>
8511
-
8512
- <!-- Shape Text Reflection Clone -->
8513
- <div v-if="(el.style as any).textReflection && (el.style as any).textReflection.enabled"
8514
- class="text-element-reflection"
8515
- style="position: absolute; left: 0; width: 100%; pointer-events: none; z-index: 1;" :style="{
8516
- top: `calc(100% + ${(el.style as any).textReflection.distance !== undefined ? (el.style as any).textReflection.distance : 20}px - ${getReflectionLeadingCompensation(el)}px)`,
8517
- transform: 'translateY(100%) scaleY(-1)',
8518
- opacity: (100 - ((el.style as any).textReflection.opacity !== undefined ? (el.style as any).textReflection.opacity : 10)) / 100,
8519
- filter: (el.style as any).textReflection.blur ? `blur(${(((el.style as any).textReflection.blur !== undefined ? (el.style as any).textReflection.blur : 0) * 1.33).toFixed(1)}px)` : 'none',
8520
- ['-webkit-mask-image']: `linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,0) ${((el.style as any).textReflection.size !== undefined ? (el.style as any).textReflection.size : 8)}%, rgba(0,0,0,0) 100%)`,
8521
- ['mask-image']: `linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,0) ${((el.style as any).textReflection.size !== undefined ? (el.style as any).textReflection.size : 8)}%, rgba(0,0,0,0) 100%)`,
8522
- color: el.style.textColor || el.style.color || (el.style.fill === '#ffffff' || el.style.fill === '#FFFFFF' ? '#475569' : '#ffffff'),
8523
- fontFamily: el.style.fontFamily || 'inherit',
8524
- fontSize: el.style.fontSize || '16px',
8525
- fontWeight: el.style.fontWeight || 'normal',
8526
- fontStyle: (el.style as any).fontStyle || 'normal',
8527
- textDecoration: (el.style.textDecoration as any) || 'none',
8528
- ['-webkit-text-stroke']: getTextStrokeStyle(el),
8529
- ['paint-order']: (el.style as any).enableTextStroke ? 'stroke fill' : 'normal',
8530
- ['text-shadow']: (el.style as any).textShadow || 'none',
8531
- lineHeight: el.style.lineHeight || 1.35,
8532
- textAlign: (el.style.textAlign as any) || 'center',
8533
- whiteSpace: (el.style as any).whiteSpace || 'normal',
8534
- transformOrigin: 'top left',
8535
- height: 'auto'
8536
- }"
8537
- v-html="el.style.textBackground && el.style.textBackground !== 'transparent'
8538
- ? `<span style='background-color: ${el.style.textBackground}; display: inline;'>${normalizeHtmlFontFamily(getSafeShapeText(el))}</span>`
8539
- : normalizeHtmlFontFamily(getSafeShapeText(el))">
8540
- </div>
8541
- </div>
8542
- </div>
8431
+ <CanvasShapeElement
8432
+ :element="el"
8433
+ :active-slide="activeSlide"
8434
+ :is-reflection="true"
8435
+ :theme-recipe="getActiveSlideThemeRecipe(activeSlide)"
8436
+ :normalize-html-font-family="normalizeHtmlFontFamily"
8437
+ :get-text-stroke-style="getTextStrokeStyle"
8438
+ :get-element-text-shadow="getElementTextShadow"
8439
+ :get-reflection-leading-compensation="getReflectionLeadingCompensation"
8440
+ :is-image-placeholder="isImagePlaceholder"
8441
+ />
8543
8442
  </template>
8544
8443
 
8545
8444
  <!-- Image element -->
@@ -122,7 +122,7 @@ function handleSelect(chartType: string, isMini = false) {
122
122
  <div class="chart-card-item" @click="handleSelect('donut')">
123
123
  <div class="chart-card-thumb">
124
124
  <div class="chart-thumb-box">
125
- <svg viewBox="0 0 36 36" width="34" height="34" fill="none">
125
+ <svg viewBox="0 0 36 36" width="72" height="72" fill="none">
126
126
  <path d="M18 5 A13 13 0 0 1 31 18 L25 18 A7 7 0 0 0 18 11 Z" fill="#2563eb" />
127
127
  <path d="M28 18 A10 10 0 0 1 18 28 L18 23 A5 5 0 0 0 23 18 Z" fill="#60a5fa" />
128
128
  <path d="M18 26 A8 8 0 0 1 10 18 L14 18 A4 4 0 0 0 18 22 Z" fill="#93c5fd" />
@@ -153,7 +153,7 @@ function handleSelect(chartType: string, isMini = false) {
153
153
  <div class="chart-card-item" @click="handleSelect('bar')">
154
154
  <div class="chart-card-thumb">
155
155
  <div class="chart-thumb-box">
156
- <svg viewBox="0 0 36 36" width="34" height="34" fill="none">
156
+ <svg viewBox="0 0 36 36" width="72" height="72" fill="none">
157
157
  <rect x="5" y="7" width="6" height="5" fill="#93c5fd" />
158
158
  <rect x="5" y="13" width="6" height="8" fill="#60a5fa" />
159
159
  <rect x="5" y="22" width="6" height="7" fill="#2563eb" />
@@ -177,7 +177,7 @@ function handleSelect(chartType: string, isMini = false) {
177
177
  <div class="chart-card-item" @click="handleSelect('bar')">
178
178
  <div class="chart-card-thumb">
179
179
  <div class="chart-thumb-box">
180
- <svg viewBox="0 0 36 36" width="34" height="34" fill="none">
180
+ <svg viewBox="0 0 36 36" width="72" height="72" fill="none">
181
181
  <rect x="5" y="16" width="6" height="6" fill="#60a5fa" />
182
182
  <rect x="5" y="23" width="6" height="6" fill="#2563eb" />
183
183
 
@@ -214,7 +214,7 @@ function handleSelect(chartType: string, isMini = false) {
214
214
  <div class="chart-card-item" @click="handleSelect('line')">
215
215
  <div class="chart-card-thumb">
216
216
  <div class="chart-thumb-box">
217
- <svg viewBox="0 0 36 36" width="34" height="34" fill="none">
217
+ <svg viewBox="0 0 36 36" width="72" height="72" fill="none">
218
218
  <path d="M5 23 L12 16 L20 21 L31 10" stroke="#2563eb" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
219
219
  <path d="M5 19 L12 23 L20 14 L31 17" stroke="#93c5fd" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
220
220
  <line x1="3" y1="28" x2="33" y2="28" stroke="#cbd5e1" stroke-width="1.2" />
@@ -228,7 +228,7 @@ function handleSelect(chartType: string, isMini = false) {
228
228
  <div class="chart-card-item" @click="handleSelect('line')">
229
229
  <div class="chart-card-thumb">
230
230
  <div class="chart-thumb-box">
231
- <svg viewBox="0 0 36 36" width="34" height="34" fill="none">
231
+ <svg viewBox="0 0 36 36" width="72" height="72" fill="none">
232
232
  <path d="M5 22 L13 22 L21 16 L31 10" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
233
233
  <line x1="3" y1="28" x2="33" y2="28" stroke="#cbd5e1" stroke-width="1.2" />
234
234
  </svg>
@@ -264,7 +264,7 @@ function handleSelect(chartType: string, isMini = false) {
264
264
  <div class="chart-card-item" @click="handleSelect('horizontal-bar')">
265
265
  <div class="chart-card-thumb">
266
266
  <div class="chart-thumb-box">
267
- <svg viewBox="0 0 36 36" width="34" height="34" fill="none">
267
+ <svg viewBox="0 0 36 36" width="72" height="72" fill="none">
268
268
  <rect x="7" y="6" width="10" height="5" fill="#2563eb" />
269
269
  <rect x="18" y="6" width="8" height="5" fill="#60a5fa" />
270
270
  <rect x="7" y="15" width="14" height="5" fill="#2563eb" />
@@ -295,7 +295,7 @@ function handleSelect(chartType: string, isMini = false) {
295
295
  <div class="chart-card-item" @click="handleSelect('area')">
296
296
  <div class="chart-card-thumb">
297
297
  <div class="chart-thumb-box">
298
- <svg viewBox="0 0 36 36" width="34" height="34" fill="none">
298
+ <svg viewBox="0 0 36 36" width="72" height="72" fill="none">
299
299
  <path d="M5 24 C10 24 15 14 20 16 C25 18 28 21 31 19 L31 29 L5 29 Z" fill="#60a5fa" opacity="0.5" />
300
300
  <path d="M5 27 C10 27 15 20 20 22 C25 24 28 26 31 25 L31 29 L5 29 Z" fill="#2563eb" opacity="0.8" />
301
301
  <line x1="3" y1="29" x2="33" y2="29" stroke="#cbd5e1" stroke-width="1.2" />
@@ -360,26 +360,33 @@ function handleSelect(chartType: string, isMini = false) {
360
360
  display: flex;
361
361
  flex-direction: column;
362
362
  width: 100%;
363
+ height: 100%;
363
364
  box-sizing: border-box;
364
365
  }
365
366
 
366
367
  .chart-category-tabs {
367
368
  display: flex;
368
369
  align-items: center;
369
- gap: 5px;
370
+ gap: 8px;
370
371
  padding-bottom: 10px;
371
372
  overflow-x: hidden;
372
373
  border-bottom: 1px solid #f1f5f9;
373
374
  }
374
375
 
375
376
  .chart-tab-pill {
376
- padding: 4px 9px;
377
+ width: 60px;
378
+ height: 36px;
379
+ padding: 0 9px;
380
+ display: inline-flex;
381
+ align-items: center;
382
+ justify-content: center;
383
+ box-sizing: border-box;
377
384
  border-radius: 6px;
378
385
  font-size: 12px;
379
- font-weight: 500;
386
+ font-weight: 400;
380
387
  color: #475569;
381
388
  background: #ffffff;
382
- border: 1px solid #e2e8f0;
389
+ border: 1px solid #E5E7EB;
383
390
  cursor: pointer;
384
391
  user-select: none;
385
392
  white-space: nowrap;
@@ -393,14 +400,16 @@ function handleSelect(chartType: string, isMini = false) {
393
400
  }
394
401
 
395
402
  .chart-tab-pill.active {
396
- background: #ebf3fe;
397
- color: #2563eb;
398
- border-color: #bfdbfe;
399
- font-weight: 600;
403
+ background: rgba(30, 111, 255, 0.12);
404
+ background-clip: padding-box;
405
+ color: #1E6FFF;
406
+ border-color: rgba(30, 111, 255, 0.12);
407
+ font-weight: 400;
400
408
  }
401
409
 
402
410
  .chart-scroll-content {
403
- max-height: 380px;
411
+ flex: 1;
412
+ min-height: 0;
404
413
  overflow-y: auto;
405
414
  overflow-x: hidden;
406
415
  padding-right: 4px;
@@ -452,8 +461,8 @@ function handleSelect(chartType: string, isMini = false) {
452
461
  }
453
462
 
454
463
  .chart-card-thumb {
455
- width: 100%;
456
- height: 68px;
464
+ width: 120px;
465
+ height: 88px;
457
466
  background: #f8fafc;
458
467
  border-radius: 6px;
459
468
  display: flex;
@@ -463,20 +472,20 @@ function handleSelect(chartType: string, isMini = false) {
463
472
  }
464
473
 
465
474
  .chart-thumb-box {
466
- width: 50px;
467
- height: 50px;
468
- border: 1px dashed #cbd5e1;
469
- border-radius: 4px;
470
- background: #ffffff;
475
+ width: 72px;
476
+ height: 72px;
477
+ border: none;
478
+ background: transparent;
471
479
  display: flex;
472
480
  align-items: center;
473
481
  justify-content: center;
474
482
  transition: all 0.15s ease;
475
483
  }
476
484
 
485
+ .chart-thumb-box svg,
477
486
  .chart-thumb-box img {
478
- width: 34px;
479
- height: 34px;
487
+ width: 72px;
488
+ height: 72px;
480
489
  object-fit: contain;
481
490
  display: block;
482
491
  }
@@ -494,11 +503,6 @@ function handleSelect(chartType: string, isMini = false) {
494
503
  background: #f1f5f9;
495
504
  }
496
505
 
497
- .chart-card-item:hover .chart-thumb-box {
498
- border-color: #3b82f6;
499
- border-style: solid;
500
- }
501
-
502
506
  .chart-card-item:hover .chart-card-name {
503
507
  color: #2563eb;
504
508
  }
@@ -1758,7 +1758,33 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
1758
1758
  const match = bgStr.match(/url\(['"]?([^'"]+)['"]?\)/)
1759
1759
  if (match && match[1]) {
1760
1760
  let bgUrl = match[1]
1761
- if (bgUrl.startsWith('data:image')) {
1761
+ if (/^data:image\/svg\+xml/i.test(bgUrl)) {
1762
+ const tiledPng = await new Promise<string | undefined>((resolve) => {
1763
+ const img = new Image()
1764
+ img.onload = () => {
1765
+ try {
1766
+ const canvas = document.createElement('canvas')
1767
+ canvas.width = Math.max(1, Math.min(4096, Math.round(logicalWidth.value || 1280)))
1768
+ canvas.height = Math.max(1, Math.min(4096, Math.round(logicalHeight.value || 720)))
1769
+ const ctx = canvas.getContext('2d')
1770
+ if (!ctx) return resolve(undefined)
1771
+ const pat = ctx.createPattern(img, 'repeat')
1772
+ if (pat) {
1773
+ ctx.fillStyle = pat
1774
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
1775
+ } else {
1776
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
1777
+ }
1778
+ resolve(canvas.toDataURL('image/png'))
1779
+ } catch {
1780
+ resolve(undefined)
1781
+ }
1782
+ }
1783
+ img.onerror = () => resolve(undefined)
1784
+ img.src = bgUrl
1785
+ })
1786
+ pptxBg = tiledPng ? { data: tiledPng } : { color: 'FFFFFF', transparency: 0 }
1787
+ } else if (bgUrl.startsWith('data:image')) {
1762
1788
  pptxBg = { data: bgUrl }
1763
1789
  } else {
1764
1790
  if (!bgUrl.startsWith('http') && !bgUrl.startsWith('data:')) {
@@ -2208,8 +2234,7 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
2208
2234
  const patternUrl = getElementPatternUrl(el)
2209
2235
  const g = getElementGradientConfig(el)
2210
2236
 
2211
- // SVG 图片填充 PowerPoint 无法可靠渲染 a:blipFill,先按形状显示尺寸栅格化为 PNG
2212
- // (画布上图案填充本身是 preserveAspectRatio="none" 拉伸铺满,与 a:stretch 语义一致)
2237
+ // SVG 图案填充 PowerPoint 无法直接渲染 SVG a:blipFill,先按形状显示尺寸以 1:1 平铺栅格化为 PNG
2213
2238
  const rasterizeSvgFill = (svgUrl: string, widthPx: number, heightPx: number): Promise<string | undefined> => {
2214
2239
  return new Promise((resolve) => {
2215
2240
  const img = new Image()
@@ -2220,7 +2245,13 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
2220
2245
  canvas.height = Math.max(1, Math.min(4096, Math.round(heightPx)))
2221
2246
  const ctx = canvas.getContext('2d')
2222
2247
  if (!ctx) return resolve(undefined)
2223
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
2248
+ const pat = ctx.createPattern(img, 'repeat')
2249
+ if (pat) {
2250
+ ctx.fillStyle = pat
2251
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
2252
+ } else {
2253
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
2254
+ }
2224
2255
  resolve(canvas.toDataURL('image/png'))
2225
2256
  } catch {
2226
2257
  resolve(undefined)
@@ -2232,8 +2263,12 @@ const generatePptxBlob = async (): Promise<Blob | null> => {
2232
2263
  }
2233
2264
 
2234
2265
  const buildNativeShapeFill = async (): Promise<any> => {
2235
- // 图片填充 → 原生 a:blipFill
2266
+ // 图片/图案填充 → 原生 a:blipFill
2236
2267
  if (patternUrl) {
2268
+ if (/^data:image\/svg\+xml/i.test(patternUrl)) {
2269
+ const pngUrl = await rasterizeSvgFill(patternUrl, pixelWidth, pixelHeight)
2270
+ return pngUrl ? { type: 'image', data: pngUrl } : undefined
2271
+ }
2237
2272
  const pUrl = await resolveMediaSrc(patternUrl, 'image')
2238
2273
  if (pUrl && /^data:image\/svg\+xml/i.test(pUrl)) {
2239
2274
  const pngUrl = await rasterizeSvgFill(pUrl, pixelWidth, pixelHeight)
@@ -26,7 +26,9 @@ import {
26
26
  getGradientAngleDeg,
27
27
  getSvgGradientCoords,
28
28
  getHexColorWithoutAlpha,
29
- getAlphaFromColor
29
+ getAlphaFromColor,
30
+ isElementPatternTile,
31
+ getElementPatternTileSize
30
32
  } from '../utils/canvas/elementStyles'
31
33
 
32
34
  const props = defineProps<{
@@ -48,6 +50,21 @@ function resolvePx(val: any, parentPx: number): number {
48
50
  const selfW = computed(() => resolvePx(props.el?.style?.width, props.parentW))
49
51
  const selfH = computed(() => resolvePx(props.el?.style?.height, props.parentH))
50
52
 
53
+ function getGroupShapePatternTransform(el: any, isTriangle = false): string | undefined {
54
+ if (!isElementPatternTile(el)) return undefined
55
+ const w = Math.max(1, selfW.value || 100)
56
+ const h = Math.max(1, selfH.value || 100)
57
+ if (isTriangle) {
58
+ return `scale(${+(100 / w).toFixed(4)}, ${+(100 / h).toFixed(4)})`
59
+ }
60
+ const vbW = el.viewBox ? el.viewBox[0] : 200
61
+ const vbH = el.viewBox ? el.viewBox[1] : 200
62
+ const bw = el.shapType === 'arc' ? 0 : (el.style?.borderWidth ? parseFloat(el.style.borderWidth) : 2)
63
+ const effW = Math.max(1, w - bw)
64
+ const effH = Math.max(1, h - bw)
65
+ return `scale(${+(vbW / effW).toFixed(4)}, ${+(vbH / effH).toFixed(4)})`
66
+ }
67
+
51
68
  const boxStyle = computed(() => {
52
69
  const el = props.el || {}
53
70
  const st = el.style || {}
@@ -326,9 +343,9 @@ const MARKER_PATHS: Record<string, string> = {
326
343
  height: '100%',
327
344
  backgroundColor: getElementPattern(el) ? 'transparent' : ((getElementGradientCss(el) || (getElementFill(el) !== undefined && getElementFill(el) !== 'none')) ? (getElementGradientCss(el) ? 'transparent' : getElementFill(el)) : (getElementFill(el) === 'none' ? 'transparent' : '#3b82f6')),
328
345
  backgroundImage: getElementPattern(el) ? `url('${getElementPattern(el)}')` : (getElementGradientCss(el) || 'none'),
329
- backgroundSize: getElementPattern(el) ? 'cover' : undefined,
330
- backgroundPosition: getElementPattern(el) ? 'center center' : undefined,
331
- backgroundRepeat: getElementPattern(el) ? 'no-repeat' : undefined,
346
+ backgroundSize: getElementPattern(el) ? (isElementPatternTile(el) ? `${getElementPatternTileSize(el).w}px ${getElementPatternTileSize(el).h}px` : 'cover') : undefined,
347
+ backgroundPosition: getElementPattern(el) ? (isElementPatternTile(el) ? '0 0' : 'center center') : undefined,
348
+ backgroundRepeat: getElementPattern(el) ? (isElementPatternTile(el) ? 'repeat' : 'no-repeat') : undefined,
332
349
  borderRadius: '50%',
333
350
  borderWidth: `${getElementBorderWidth(el)}px`,
334
351
  borderStyle: el.style?.borderStyle || (el as any).borderType || 'solid',
@@ -341,9 +358,16 @@ const MARKER_PATHS: Record<string, string> = {
341
358
  <svg viewBox="0 0 100 100" preserveAspectRatio="none"
342
359
  style="width: 100%; height: 100%; display: block; overflow: visible;">
343
360
  <defs>
344
- <pattern v-if="getElementPattern(el)" :id="`pres-group-shape-pattern-${el.id}`" width="100%" height="100%"
345
- patternContentUnits="objectBoundingBox">
346
- <image :href="getElementPattern(el)" width="1" height="1" preserveAspectRatio="none" />
361
+ <pattern v-if="getElementPattern(el)" :id="`pres-group-shape-pattern-${el.id}`"
362
+ :width="isElementPatternTile(el) ? getElementPatternTileSize(el).w : '100%'"
363
+ :height="isElementPatternTile(el) ? getElementPatternTileSize(el).h : '100%'"
364
+ :patternUnits="isElementPatternTile(el) ? 'userSpaceOnUse' : 'objectBoundingBox'"
365
+ :patternContentUnits="isElementPatternTile(el) ? 'userSpaceOnUse' : 'objectBoundingBox'"
366
+ :patternTransform="getGroupShapePatternTransform(el, true)">
367
+ <image :href="getElementPattern(el)"
368
+ :width="isElementPatternTile(el) ? getElementPatternTileSize(el).w : 1"
369
+ :height="isElementPatternTile(el) ? getElementPatternTileSize(el).h : 1"
370
+ preserveAspectRatio="none" />
347
371
  </pattern>
348
372
  <template v-if="getElementGradientConfig(el)">
349
373
  <radialGradient
@@ -397,9 +421,16 @@ const MARKER_PATHS: Record<string, string> = {
397
421
  style="width: 100%; height: 100%; overflow: visible;">
398
422
  <svg overflow="visible" style="width: 100%; height: 100%; display: block; overflow: visible;">
399
423
  <defs>
400
- <pattern v-if="getElementPattern(el)" :id="`pres-group-shape-pattern-${el.id}`" width="100%" height="100%"
401
- patternContentUnits="objectBoundingBox">
402
- <image :href="getElementPattern(el)" width="1" height="1" preserveAspectRatio="none" />
424
+ <pattern v-if="getElementPattern(el)" :id="`pres-group-shape-pattern-${el.id}`"
425
+ :width="isElementPatternTile(el) ? getElementPatternTileSize(el).w : '100%'"
426
+ :height="isElementPatternTile(el) ? getElementPatternTileSize(el).h : '100%'"
427
+ :patternUnits="isElementPatternTile(el) ? 'userSpaceOnUse' : 'objectBoundingBox'"
428
+ :patternContentUnits="isElementPatternTile(el) ? 'userSpaceOnUse' : 'objectBoundingBox'"
429
+ :patternTransform="getGroupShapePatternTransform(el, false)">
430
+ <image :href="getElementPattern(el)"
431
+ :width="isElementPatternTile(el) ? getElementPatternTileSize(el).w : 1"
432
+ :height="isElementPatternTile(el) ? getElementPatternTileSize(el).h : 1"
433
+ preserveAspectRatio="none" />
403
434
  </pattern>
404
435
  <template v-if="getElementGradientConfig(el)">
405
436
  <radialGradient
@@ -458,9 +489,9 @@ const MARKER_PATHS: Record<string, string> = {
458
489
  height: '100%',
459
490
  backgroundColor: getElementPattern(el) ? 'transparent' : ((getElementGradientCss(el) || (getElementFill(el) !== undefined && getElementFill(el) !== 'none')) ? (getElementGradientCss(el) ? 'transparent' : getElementFill(el)) : (getElementFill(el) === 'none' ? 'transparent' : '#10b981')),
460
491
  backgroundImage: getElementPattern(el) ? `url('${getElementPattern(el)}')` : (getElementGradientCss(el) || 'none'),
461
- backgroundSize: getElementPattern(el) ? 'cover' : undefined,
462
- backgroundPosition: getElementPattern(el) ? 'center center' : undefined,
463
- backgroundRepeat: getElementPattern(el) ? 'no-repeat' : undefined,
492
+ backgroundSize: getElementPattern(el) ? (isElementPatternTile(el) ? `${getElementPatternTileSize(el).w}px ${getElementPatternTileSize(el).h}px` : 'cover') : undefined,
493
+ backgroundPosition: getElementPattern(el) ? (isElementPatternTile(el) ? '0 0' : 'center center') : undefined,
494
+ backgroundRepeat: getElementPattern(el) ? (isElementPatternTile(el) ? 'repeat' : 'no-repeat') : undefined,
464
495
  borderRadius: el.style?.borderRadius !== undefined ? el.style.borderRadius : '0px',
465
496
  borderWidth: `${getElementBorderWidth(el)}px`,
466
497
  borderStyle: el.style?.borderStyle || (el as any).borderType || 'solid',