Lashtw commited on
Commit
d18944f
·
verified ·
1 Parent(s): f444fb2

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +189 -28
index.html CHANGED
@@ -384,12 +384,37 @@
384
  <div>
385
  <div class="flex justify-between items-center mb-1">
386
  <label class="block text-sm font-bold text-slate-700">5. 自定義圖片</label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  <button
388
- @click="triggerImageUpload"
389
- class="text-xs bg-indigo-100 text-indigo-700 px-2 py-1 rounded hover:bg-indigo-200 font-bold flex items-center gap-1"
390
  >
391
- <svg viewBox="0 0 512 512" class="w-3 h-3 fill-current mr-1"><path d="M0 96C0 60.7 28.7 32 64 32H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6h96 32H424c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192c26.5 0 48-21.5 48-48s-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48z"/></svg>
392
- 上傳圖片
 
 
 
 
 
 
 
 
 
393
  </button>
394
  <input type="file" ref="imageUploadInput" class="hidden" accept="image/*" @change="handleImageUpload">
395
  </div>
@@ -585,7 +610,7 @@
585
  <span v-if="cell.type === 'icon'" class="text-2xl leading-none select-none">{{ icons[cell.content] }}</span>
586
 
587
  <!-- Image Renderer -->
588
- <img v-if="cell.type === 'image'" :src="cell.content" class="w-4/5 h-4/5 object-contain">
589
  </div>
590
  </div>
591
  </div>
@@ -645,7 +670,8 @@
645
  <span v-if="cell.type === 'icon'" class="text-5xl md:text-7xl leading-none select-none">{{ icons[cell.content] }}</span>
646
 
647
  <!-- Image Renderer -->
648
- <img v-if="cell.type === 'image'" :src="cell.content" class="w-4/5 h-4/5 object-contain">
 
649
  </div>
650
  </div>
651
  </div>
@@ -733,7 +759,8 @@
733
  content: '',
734
  rotation: 0,
735
  color: '#1e293b', // Default text color
736
- bgColor: '#ffffff' // Default bg color
 
737
  }));
738
 
739
  const pages = ref([
@@ -778,6 +805,18 @@
778
  const tempImageSrc = ref('');
779
  const customImages = ref([]); // Stores base64 strings
780
  let cropperInstance = null;
 
 
 
 
 
 
 
 
 
 
 
 
781
 
782
  // --- Auto-Save Key ---
783
  const AUTOSAVE_KEY = 'magic_origami_autosave_v1';
@@ -900,7 +939,7 @@
900
  }
901
  };
902
 
903
- const setCell = (pageIndex, row, col, type, content, rotation = 0, color = '#1e293b', bgColor='#ffffff') => {
904
  if (row > currentGrid.value.rows || col > currentGrid.value.cols) return;
905
  const cells = pages.value[pageIndex].cells;
906
  const index = (row - 1) * currentGrid.value.cols + (col - 1);
@@ -910,6 +949,7 @@
910
  cells[index].rotation = rotation;
911
  cells[index].color = color;
912
  cells[index].bgColor = bgColor;
 
913
  }
914
  };
915
 
@@ -927,9 +967,17 @@
927
  });
928
  };
929
 
 
 
 
 
 
 
 
930
  // --- Image Upload Logic ---
931
 
932
- const triggerImageUpload = () => {
 
933
  imageUploadInput.value.click();
934
  };
935
 
@@ -943,15 +991,53 @@
943
  showCropper.value = true;
944
  event.target.value = '';
945
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
946
  nextTick(() => {
947
  if (cropperInstance) cropperInstance.destroy();
948
- cropperInstance = new Cropper(cropperImgRef.value, {
949
- aspectRatio: 1,
950
  viewMode: 1,
951
  dragMode: 'move',
952
  autoCropArea: 0.9,
953
  background: false
954
- });
 
 
 
 
 
 
 
 
955
  });
956
  };
957
  reader.readAsDataURL(file);
@@ -969,13 +1055,67 @@
969
  const confirmCrop = () => {
970
  if (!cropperInstance) return;
971
 
972
- const canvas = cropperInstance.getCroppedCanvas({
973
- width: 300,
974
- height: 300
975
- });
976
-
977
- const base64 = canvas.toDataURL('image/jpeg', 0.85);
978
- customImages.value.push(base64);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
979
 
980
  cancelCrop();
981
  };
@@ -985,6 +1125,8 @@
985
  const cell = activePageCells.value[index];
986
  cell.type = 'image';
987
  cell.content = imgSrc;
 
 
988
  });
989
  inputBuffer.value = '';
990
  batchInputBuffer.value = '';
@@ -1449,7 +1591,11 @@
1449
  const iconChar = icons[cell.content];
1450
  contentHtml = `<span class="export-text" data-color="${cell.color}" data-size="50" style="font-size: 50px; line-height: 1; user-select: none;">${iconChar}</span>`;
1451
  } else if (cell.type === 'image') {
1452
- contentHtml = `<img src="${cell.content}" style="width: 80%; height: 80%; object-fit: contain;">`;
 
 
 
 
1453
  }
1454
 
1455
  // New: Inner Highlight for PDF Export
@@ -1618,15 +1764,27 @@
1618
  });
1619
  } else if (cell.type === 'image') {
1620
  // Image
1621
- // Calculate image size to fit within cell (80% padding like HTML)
1622
- const padW = cellW * 0.1;
1623
- const padH = cellH * 0.1;
 
 
 
 
 
 
 
 
 
 
 
 
1624
  slide.addImage({
1625
  data: cell.content,
1626
- x: x + padW,
1627
- y: y + padH,
1628
- w: cellW * 0.8,
1629
- h: cellH * 0.8,
1630
  rotate: cell.rotation
1631
  });
1632
  }
@@ -1756,7 +1914,10 @@
1756
  toggleMultiSelectMode,
1757
  clearSelection,
1758
  getSelectionOrder,
1759
- selectedIndicesArray
 
 
 
1760
  };
1761
  }
1762
  }).mount('#app');
 
384
  <div>
385
  <div class="flex justify-between items-center mb-1">
386
  <label class="block text-sm font-bold text-slate-700">5. 自定義圖片</label>
387
+
388
+ <!-- Toggle for Image Fit/Fill -->
389
+ <div v-if="selectedCellIndices.size > 0" class="flex items-center gap-1">
390
+ <button
391
+ @click="toggleImageScale"
392
+ class="text-xs px-2 py-1 rounded border transition-all"
393
+ :class="currentImageScale === 'fill' ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-slate-500 border-slate-300 hover:bg-slate-50'"
394
+ title="切換圖片填充模式:適中 / 滿版"
395
+ >
396
+ <i class="fa-solid" :class="currentImageScale === 'fill' ? 'fa-expand' : 'fa-compress'"></i>
397
+ {{ currentImageScale === 'fill' ? '滿版' : '適中' }}
398
+ </button>
399
+ </div>
400
+ </div>
401
+
402
+ <div class="flex gap-2 mb-2">
403
  <button
404
+ @click="triggerImageUpload('single')"
405
+ class="flex-1 text-xs bg-indigo-100 text-indigo-700 px-2 py-2 rounded hover:bg-indigo-200 font-bold flex items-center justify-center gap-1"
406
  >
407
+ <svg viewBox="0 0 512 512" class="w-3 h-3 fill-current"><path d="M0 96C0 60.7 28.7 32 64 32H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6h96 32H424c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192c26.5 0 48-21.5 48-48s-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48z"/></svg>
408
+ 單圖上傳
409
+ </button>
410
+ <!-- Puzzle Upload Button (Visible only when multiple cells selected) -->
411
+ <button
412
+ v-if="selectedCellIndices.size > 1"
413
+ @click="triggerImageUpload('puzzle')"
414
+ class="flex-1 text-xs bg-purple-100 text-purple-700 px-2 py-2 rounded hover:bg-purple-200 font-bold flex items-center justify-center gap-1"
415
+ >
416
+ <i class="fa-solid fa-puzzle-piece"></i>
417
+ 拼圖上傳 (自動分割)
418
  </button>
419
  <input type="file" ref="imageUploadInput" class="hidden" accept="image/*" @change="handleImageUpload">
420
  </div>
 
610
  <span v-if="cell.type === 'icon'" class="text-2xl leading-none select-none">{{ icons[cell.content] }}</span>
611
 
612
  <!-- Image Renderer -->
613
+ <img v-if="cell.type === 'image'" :src="cell.content" class="w-full h-full" :style="{ objectFit: cell.imageScale === 'fill' ? 'cover' : 'contain', width: cell.imageScale === 'fill' ? '100%' : '80%', height: cell.imageScale === 'fill' ? '100%' : '80%' }">
614
  </div>
615
  </div>
616
  </div>
 
670
  <span v-if="cell.type === 'icon'" class="text-5xl md:text-7xl leading-none select-none">{{ icons[cell.content] }}</span>
671
 
672
  <!-- Image Renderer -->
673
+ <!-- Dynamic Style for Fit/Fill -->
674
+ <img v-if="cell.type === 'image'" :src="cell.content" class="w-full h-full" :style="{ objectFit: cell.imageScale === 'fill' ? 'cover' : 'contain', width: cell.imageScale === 'fill' ? '100%' : '80%', height: cell.imageScale === 'fill' ? '100%' : '80%' }">
675
  </div>
676
  </div>
677
  </div>
 
759
  content: '',
760
  rotation: 0,
761
  color: '#1e293b', // Default text color
762
+ bgColor: '#ffffff', // Default bg color
763
+ imageScale: 'fit' // 'fit' (contain, with padding) or 'fill' (cover, no padding)
764
  }));
765
 
766
  const pages = ref([
 
805
  const tempImageSrc = ref('');
806
  const customImages = ref([]); // Stores base64 strings
807
  let cropperInstance = null;
808
+
809
+ // Puzzle Upload State
810
+ const isPuzzleUpload = ref(false);
811
+ const puzzleConfig = ref({ rows: 1, cols: 1, targetAspectRatio: 1 });
812
+
813
+ // Image Scale State (for UI toggle)
814
+ const currentImageScale = computed(() => {
815
+ if (selectedCellIndices.value.size === 0) return 'fit';
816
+ // Check first selected cell
817
+ const idx = [...selectedCellIndices.value][0];
818
+ return activePageCells.value[idx].imageScale || 'fit';
819
+ });
820
 
821
  // --- Auto-Save Key ---
822
  const AUTOSAVE_KEY = 'magic_origami_autosave_v1';
 
939
  }
940
  };
941
 
942
+ const setCell = (pageIndex, row, col, type, content, rotation = 0, color = '#1e293b', bgColor='#ffffff', imageScale='fit') => {
943
  if (row > currentGrid.value.rows || col > currentGrid.value.cols) return;
944
  const cells = pages.value[pageIndex].cells;
945
  const index = (row - 1) * currentGrid.value.cols + (col - 1);
 
949
  cells[index].rotation = rotation;
950
  cells[index].color = color;
951
  cells[index].bgColor = bgColor;
952
+ cells[index].imageScale = imageScale;
953
  }
954
  };
955
 
 
967
  });
968
  };
969
 
970
+ const toggleImageScale = () => {
971
+ const newScale = currentImageScale.value === 'fit' ? 'fill' : 'fit';
972
+ selectedCellIndices.value.forEach(index => {
973
+ activePageCells.value[index].imageScale = newScale;
974
+ });
975
+ };
976
+
977
  // --- Image Upload Logic ---
978
 
979
+ const triggerImageUpload = (mode = 'single') => {
980
+ isPuzzleUpload.value = (mode === 'puzzle');
981
  imageUploadInput.value.click();
982
  };
983
 
 
991
  showCropper.value = true;
992
  event.target.value = '';
993
 
994
+ // Logic for Puzzle Mode aspect ratio
995
+ let aspectRatio = 1; // default square
996
+ if (isPuzzleUpload.value && selectedCellIndices.value.size > 1) {
997
+ // Calculate bounds
998
+ const indices = [...selectedCellIndices.value];
999
+ const cols = currentGrid.value.cols;
1000
+ const rows = currentGrid.value.rows;
1001
+
1002
+ // Map to Row/Col coords
1003
+ const coords = indices.map(idx => ({ r: Math.floor(idx / cols), c: idx % cols }));
1004
+ const minR = Math.min(...coords.map(p => p.r));
1005
+ const maxR = Math.max(...coords.map(p => p.r));
1006
+ const minC = Math.min(...coords.map(p => p.c));
1007
+ const maxC = Math.max(...coords.map(p => p.c));
1008
+
1009
+ const selRows = maxR - minR + 1;
1010
+ const selCols = maxC - minC + 1;
1011
+
1012
+ // Calculate A4-based aspect ratio
1013
+ // A4 W=210, H=297.
1014
+ // CellW = 210/cols, CellH = 297/rows
1015
+ // BlockW = selCols * (210/cols)
1016
+ // BlockH = selRows * (297/rows)
1017
+ const blockW = selCols * (210 / cols);
1018
+ const blockH = selRows * (297 / rows);
1019
+
1020
+ aspectRatio = blockW / blockH;
1021
+ puzzleConfig.value = { rows: selRows, cols: selCols, minR, minC, targetAspectRatio: aspectRatio };
1022
+ }
1023
+
1024
  nextTick(() => {
1025
  if (cropperInstance) cropperInstance.destroy();
1026
+
1027
+ const options = {
1028
  viewMode: 1,
1029
  dragMode: 'move',
1030
  autoCropArea: 0.9,
1031
  background: false
1032
+ };
1033
+
1034
+ if (isPuzzleUpload.value) {
1035
+ options.aspectRatio = puzzleConfig.value.targetAspectRatio;
1036
+ } else {
1037
+ options.aspectRatio = 1; // Default square for single cell
1038
+ }
1039
+
1040
+ cropperInstance = new Cropper(cropperImgRef.value, options);
1041
  });
1042
  };
1043
  reader.readAsDataURL(file);
 
1055
  const confirmCrop = () => {
1056
  if (!cropperInstance) return;
1057
 
1058
+ if (isPuzzleUpload.value) {
1059
+ // --- Puzzle Mode Logic ---
1060
+ const canvas = cropperInstance.getCroppedCanvas();
1061
+ const ctx = canvas.getContext('2d');
1062
+
1063
+ const imgW = canvas.width;
1064
+ const imgH = canvas.height;
1065
+ const pRows = puzzleConfig.value.rows;
1066
+ const pCols = puzzleConfig.value.cols;
1067
+ const pieceW = imgW / pCols;
1068
+ const pieceH = imgH / pRows;
1069
+
1070
+ // Iterate through selected cells and assign pieces
1071
+ const indices = [...selectedCellIndices.value];
1072
+ const gridCols = currentGrid.value.cols;
1073
+
1074
+ indices.forEach(idx => {
1075
+ const r = Math.floor(idx / gridCols);
1076
+ const c = idx % gridCols;
1077
+
1078
+ // Calculate relative position in the selection block
1079
+ const relR = r - puzzleConfig.value.minR;
1080
+ const relC = c - puzzleConfig.value.minC;
1081
+
1082
+ // Only process if within valid relative bounds (simple check)
1083
+ if (relR >= 0 && relR < pRows && relC >= 0 && relC < pCols) {
1084
+ // Create a temporary canvas for this piece
1085
+ const pCanvas = document.createElement('canvas');
1086
+ pCanvas.width = pieceW;
1087
+ pCanvas.height = pieceH;
1088
+ const pCtx = pCanvas.getContext('2d');
1089
+
1090
+ pCtx.drawImage(canvas,
1091
+ relC * pieceW, relR * pieceH, pieceW, pieceH, // Source
1092
+ 0, 0, pieceW, pieceH // Dest
1093
+ );
1094
+
1095
+ const pieceData = pCanvas.toDataURL('image/jpeg', 0.9);
1096
+
1097
+ // Update cell
1098
+ const cell = activePageCells.value[idx];
1099
+ cell.type = 'image';
1100
+ cell.content = pieceData;
1101
+ cell.imageScale = 'fill'; // Force fill for puzzle
1102
+ }
1103
+ });
1104
+
1105
+ // Save to custom images list (store the whole one or pieces? currently just pieces in cells)
1106
+ // If we want to add to "Custom Images" panel, maybe add the full one?
1107
+ // For simplicity, we just apply to grid.
1108
+
1109
+ } else {
1110
+ // --- Single Mode Logic ---
1111
+ const canvas = cropperInstance.getCroppedCanvas({
1112
+ width: 300,
1113
+ height: 300
1114
+ });
1115
+ const base64 = canvas.toDataURL('image/jpeg', 0.85);
1116
+ customImages.value.push(base64);
1117
+ applyCustomImageToCell(base64);
1118
+ }
1119
 
1120
  cancelCrop();
1121
  };
 
1125
  const cell = activePageCells.value[index];
1126
  cell.type = 'image';
1127
  cell.content = imgSrc;
1128
+ // Keep existing scale preference or default to fit for manual apply
1129
+ // cell.imageScale = 'fit';
1130
  });
1131
  inputBuffer.value = '';
1132
  batchInputBuffer.value = '';
 
1591
  const iconChar = icons[cell.content];
1592
  contentHtml = `<span class="export-text" data-color="${cell.color}" data-size="50" style="font-size: 50px; line-height: 1; user-select: none;">${iconChar}</span>`;
1593
  } else if (cell.type === 'image') {
1594
+ // Check if image scale is 'fill' (Full Bleed)
1595
+ const imgStyle = cell.imageScale === 'fill'
1596
+ ? 'width: 100%; height: 100%; object-fit: cover;'
1597
+ : 'width: 80%; height: 80%; object-fit: contain;';
1598
+ contentHtml = `<img src="${cell.content}" style="${imgStyle}">`;
1599
  }
1600
 
1601
  // New: Inner Highlight for PDF Export
 
1764
  });
1765
  } else if (cell.type === 'image') {
1766
  // Image
1767
+ let imgX = x, imgY = y, imgW = cellW, imgH = cellH;
1768
+
1769
+ // Check fill vs fit
1770
+ if (cell.imageScale === 'fill') {
1771
+ // Full Bleed: x, y, w, h are full cell
1772
+ } else {
1773
+ // Fit: 80% with padding
1774
+ const padW = cellW * 0.1;
1775
+ const padH = cellH * 0.1;
1776
+ imgX += padW;
1777
+ imgY += padH;
1778
+ imgW *= 0.8;
1779
+ imgH *= 0.8;
1780
+ }
1781
+
1782
  slide.addImage({
1783
  data: cell.content,
1784
+ x: imgX,
1785
+ y: imgY,
1786
+ w: imgW,
1787
+ h: imgH,
1788
  rotate: cell.rotation
1789
  });
1790
  }
 
1914
  toggleMultiSelectMode,
1915
  clearSelection,
1916
  getSelectionOrder,
1917
+ selectedIndicesArray,
1918
+ // Puzzle
1919
+ toggleImageScale,
1920
+ currentImageScale
1921
  };
1922
  }
1923
  }).mount('#app');