Lashtw commited on
Commit
214de24
·
verified ·
1 Parent(s): d86a13d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +432 -277
index.html CHANGED
@@ -15,6 +15,10 @@
15
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
16
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
17
 
 
 
 
 
18
  <!-- Google Fonts -->
19
  <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&display=swap" rel="stylesheet">
20
 
@@ -65,6 +69,21 @@
65
  left: -9999px;
66
  }
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  .no-scrollbar::-webkit-scrollbar {
69
  display: none;
70
  }
@@ -72,6 +91,12 @@
72
  -ms-overflow-style: none;
73
  scrollbar-width: none;
74
  }
 
 
 
 
 
 
75
  </style>
76
  </head>
77
  <body class="h-screen overflow-hidden text-slate-800">
@@ -81,7 +106,7 @@
81
  <!-- =======================
82
  LEFT PANEL: Controls
83
  ======================= -->
84
- <div class="w-full md:w-1/3 lg:w-1/4 bg-white border-r border-slate-200 flex flex-col h-full shadow-lg z-20">
85
 
86
  <!-- Header -->
87
  <div class="p-6 border-b border-slate-100 bg-slate-50">
@@ -114,7 +139,7 @@
114
  </div>
115
 
116
  <!-- Scrollable Content -->
117
- <div class="flex-1 overflow-y-auto p-6 space-y-8">
118
 
119
  <!-- Grid Settings -->
120
  <div>
@@ -130,7 +155,7 @@
130
  {{ conf.label }}
131
  </button>
132
  </div>
133
- <!-- New Clear All Button -->
134
  <button
135
  @click="clearAllContent"
136
  class="w-full py-2 text-sm text-red-600 border border-red-200 bg-red-50 hover:bg-red-100 rounded-lg transition-all flex items-center justify-center gap-1"
@@ -149,16 +174,14 @@
149
  @click="applyTemplate('lucky')"
150
  class="w-full py-3 bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 text-white font-bold rounded-lg shadow-md hover:shadow-lg active:scale-95 transition-all flex items-center justify-center gap-2"
151
  >
152
- <!-- 更新這裡的 SVG 為新的帶梗幸運草 -->
153
- <svg viewBox="0 0 24 24" class="w-5 h-5 fill-current">
154
- <path d="M12,12 m-2,-2 a3,3 0 0,1 6,0 a3,3 0 0,1 -6,0 M12,12 m2,-2 a3,3 0 0,1 0,6 a3,3 0 0,1 0,-6 M12,12 m2,2 a3,3 0 0,1 -6,0 a3,3 0 0,1 6,0 M12,12 m-2,2 a3,3 0 0,1 0,-6 a3,3 0 0,1 0,6 M14,14 Q18,18 20,24 L22,23 Q19,16 14,14 Z"></path>
155
- </svg>
156
- 套用:幸運遇見你 (如皓老師版)
157
  </button>
158
  <p class="text-xs text-slate-400 mt-2">提示:套用後會自動切換為 6x8 網格並填入內容。</p>
159
  </div>
160
 
161
- <!-- Project Management (New Feature) -->
162
  <div class="border-t border-slate-200 pt-4">
163
  <label class="block text-sm font-bold text-slate-700 mb-2">💾 專案管理</label>
164
  <div class="grid grid-cols-2 gap-2">
@@ -188,7 +211,6 @@
188
  @change="importProject"
189
  >
190
  </div>
191
- <p class="text-xs text-slate-400 mt-2">下載 .json 檔可保留目前進度,下次上課可匯入繼續編輯。</p>
192
  </div>
193
 
194
  <!-- Instruction -->
@@ -203,7 +225,7 @@
203
  </div>
204
 
205
  <!-- Editing Controls -->
206
- <div v-if="viewMode === 'edit'" class="space-y-8 animate-fade-in">
207
  <!-- Text Input -->
208
  <div>
209
  <label class="block text-sm font-bold text-slate-700 mb-2">1. 文字輸入</label>
@@ -225,7 +247,7 @@
225
  <label class="block text-sm font-bold text-slate-700 mb-3">2. 選擇圖示</label>
226
  <div class="grid grid-cols-4 gap-2 max-h-60 overflow-y-auto p-1 custom-scrollbar">
227
  <button
228
- v-for="(iconPath, name) in icons"
229
  :key="name"
230
  @click="applyIconToCell(name)"
231
  :disabled="selectedCellIndex === null"
@@ -233,9 +255,42 @@
233
  :class="selectedCellIndex === null ? 'border-slate-200 text-slate-300 cursor-not-allowed' : 'border-slate-300 text-slate-600 hover:border-indigo-400 hover:text-indigo-600 cursor-pointer'"
234
  :title="name"
235
  >
236
- <svg viewBox="0 0 24 24" class="w-8 h-8 fill-current">
237
- <path :d="iconPath"></path>
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  </svg>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  </button>
240
  </div>
241
  </div>
@@ -286,6 +341,18 @@
286
  </span>
287
  </button>
288
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
289
  </div>
290
 
291
  <!-- =======================
@@ -304,7 +371,6 @@
304
  style="width: 120mm; height: 170mm;"
305
  @click="switchToPage(pageId)"
306
  >
307
- <!-- Added :key to force re-render when grid changes -->
308
  <div
309
  class="grid gap-0 border border-slate-200 w-full h-full pointer-events-none"
310
  :key="currentGrid.label"
@@ -320,7 +386,12 @@
320
  >
321
  <div class="rotation-wrapper" :style="{ transform: `rotate(${cell.rotation}deg)` }">
322
  <span v-if="cell.type === 'text'" class="text-2xl font-bold text-slate-800 cell-text">{{ cell.content }}</span>
323
- <svg v-if="cell.type === 'icon'" viewBox="0 0 24 24" class="w-3/5 h-3/5 fill-slate-800"><path :d="icons[cell.content]"></path></svg>
 
 
 
 
 
324
  </div>
325
  </div>
326
  </div>
@@ -343,7 +414,6 @@
343
  Page {{ activePageId }} - {{ currentGrid.rows }}x{{ currentGrid.cols }}
344
  </div>
345
 
346
- <!-- Added :key here too -->
347
  <div
348
  class="grid gap-0 border border-slate-200 w-full h-full"
349
  :key="currentGrid.label + activePageId"
@@ -368,7 +438,12 @@
368
  <span v-if="cell.type === 'text'" class="text-4xl md:text-5xl font-bold text-slate-800 cell-text block">
369
  {{ cell.content }}
370
  </span>
371
- <svg v-if="cell.type === 'icon'" viewBox="0 0 24 24" class="w-3/5 h-3/5 fill-slate-800"><path :d="icons[cell.content]"></path></svg>
 
 
 
 
 
372
  </div>
373
  </div>
374
  </div>
@@ -394,7 +469,7 @@
394
  </div>
395
 
396
  <script>
397
- const { createApp, ref, computed, nextTick } = Vue;
398
  const { jsPDF } = window.jspdf;
399
 
400
  createApp({
@@ -407,84 +482,80 @@
407
  ];
408
  const currentGrid = ref(gridOptions[1]); // Default 6x8
409
 
410
- // --- Icons (Updated Clover) ---
 
411
  const icons = {
412
- // 再次修正幸運草:縮小葉片範圍,確保右下角的梗(stem)不會被覆蓋
413
- // 使用四個圓形/心形組成中心,加上明顯的右下角彎曲梗
414
- '幸運草': `
415
- M9,12 a3,3 0 1,1 6,0 a3,3 0 1,1 -6,0
416
- M12,9 a3,3 0 1,1 0,6 a3,3 0 1,1 0,-6
417
- M9,9 a3,3 0 1,1 6,0 a3,3 0 1,1 -6,0
418
- M12,12 a3,3 0 1,1 -6,0 a3,3 0 1,1 6,0
419
- M13.5,13.5 Q17,17 19,23 L21,22 Q18,15 13.5,13.5 Z
420
- `,
421
- '愛心': 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z',
422
- '星星': 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z',
423
- '勝利': 'M16.5 13c-.55 0-1 .45-1 1v5h-1v-5c0-.55-.45-1-1-1s-1 .45-1 1v5h-1v-5c0-.55-.45-1-1-1s-1 .45-1 1v2.5c0 .55-.45 1-1 1s-1-.45-1-1V5.63c0-1-.7-1.63-1.6-1.63-.88 0-1.6.82-1.6 1.73V13h-1V5.73C5.3 3.66 7.03 2 9.1 2c1.77 0 3.32 1.22 3.8 2.87.66-1.07 1.8-1.87 3.1-1.87 2.21 0 4 1.79 4 4v6c0 .55-.45 1-1 1s-1-.45-1-1v-1h-1.5v1z M21 16c0 2.97-2.16 5.43-5 5.91V22h-8v-2.09c-2.84-.48-5-2.94-5-5.91h2c0 2.76 2.24 5 5 5s5-2.24 5-5h2z',
424
- '獎盃': 'M20.2 6.5C19.7 3.9 17.5 2 15 2H9C6.5 2 4.3 3.9 3.8 6.5L3 11c-.5 2.5 1.2 5 3.8 5.8V17c0 2.2 1.8 4 4 4h2.4c2.2 0 4-1.8 4-4v-.2c2.6-.8 4.3-3.3 3.8-5.8l-.8-4.5zM6.5 10.6l.6-3.8C7.4 4.8 9.1 4 9 4h6c-.1 0 1.6.8 1.9 2.8l.6 3.8c.2 1.3-.8 2.4-2.1 2.4h-9c-1.3 0-2.3-1.1-2.1-2.4z M17 17c0 1.1-.9 2-2 2H9c-1.1 0-2-.9-2-2v-1h10v1z M15 22H9v1h6v-1z',
425
- '笑臉': 'M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z',
426
- '皇冠': 'M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11h-14zm14 2H5v1c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-1z',
427
- '鑽石': 'M19 3H5L2 9l10 12L22 9l-3-6zM9.62 10l1.5-5h1.76l1.5 5H9.62zM12 18.5L6.37 11h11.26L12 18.5z',
428
- '燈泡': 'M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z',
429
- '太陽': 'M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm9-9.95h-2V3.5h2V.55zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 10.5v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19.5h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z',
430
- '月亮': 'M12 2C9.55 2 7.31 2.91 5.61 4.39 8.27 5.1 10.26 7.42 10.26 10.26c0 3.31-2.69 6-6 6-1.07 0-2.07-.26-2.96-.71C2.92 18.25 5.28 20 8 20c4.42 0 8-3.58 8-8s-3.58-8-8-8z',
431
- '雲朵': 'M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z',
432
- '音符': 'M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z',
433
- '飛機': 'M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z',
434
- '花朵': 'M12 2c-3.31 0-6 2.69-6 6 0 1.95.95 3.67 2.41 4.77C6.95 13.87 6 15.82 6 18c0 3.31 2.69 6 6 6s6-2.69 6-6c0-2.18-.95-4.13-2.41-5.23C17.05 11.67 18 9.95 18 8c0-3.31-2.69-6-6-6zm0 10c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z',
435
- '樹木': 'M12 2L2 12h3v8h6v-6h2v6h6v-8h3L12 2z',
436
- '禮物': 'M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.54-2.5 1.35l-.5.67-.5-.68C10.96 2.54 10.05 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm11 15H4v-2h16v2zm0-5H4V8h5.08L7 10.83 8.62 12 11 8.76 13.38 12 15 10.83 12.92 8H20v6z'
437
- };
438
-
439
- // --- State Initialization ---
440
- // Helper to create empty cells
441
- const createPageCells = (pageOffset, rows, cols) => Array.from({ length: rows * cols }, (_, i) => ({
442
- id: pageOffset + i,
443
- type: 'text',
444
- content: '',
445
- rotation: 0
446
- }));
447
 
448
- const pages = ref([
449
- { id: 1, cells: createPageCells(0, 8, 6) },
450
- { id: 2, cells: createPageCells(48, 8, 6) }
451
- ]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
 
453
- const viewMode = ref('overview');
454
- const activePageId = ref(1);
455
- const selectedCellIndex = ref(null);
456
- const inputBuffer = ref('');
457
- const isGenerating = ref(false);
458
- const textInputRef = ref(null);
459
- const fileInputRef = ref(null); // Reference for file input
460
 
461
- const activePageCells = computed(() => pages.value[activePageId.value - 1].cells);
462
 
463
- // --- Actions ---
464
 
465
  const changeGridSize = (conf) => {
466
- // 如果是重複點擊目前的網格,不做任何事
467
  if (currentGrid.value.label === conf.label) return;
468
 
469
- // 檢查是否有內容 (防止誤觸清空)
470
  const hasContent = pages.value.some(p => p.cells.some(c => c.content !== ''));
471
-
472
  if (hasContent) {
473
  alert("若要切換網格設定請清除內容後再切換。");
474
  return;
475
  }
476
 
477
- // 執行切換 (無內容時直接切換)
478
  currentGrid.value = conf;
479
  const totalCells = conf.rows * conf.cols;
480
 
481
- // Recreate pages with new dimensions
482
  pages.value = [
483
  { id: 1, cells: createPageCells(0, conf.rows, conf.cols) },
484
  { id: 2, cells: createPageCells(totalCells, conf.rows, conf.cols) }
485
  ];
486
 
487
- // Reset view state
488
  selectedCellIndex.value = null;
489
  activePageId.value = 1;
490
  viewMode.value = 'overview';
@@ -498,7 +569,6 @@
498
  }
499
 
500
  if (confirm("確定要進行一鍵清空嗎?所有編輯的內容將會消失且無法復原。")) {
501
- // 重置頁面內容但保留目前的網格設定
502
  const rows = currentGrid.value.rows;
503
  const cols = currentGrid.value.cols;
504
  const totalCells = rows * cols;
@@ -508,42 +578,108 @@
508
  { id: 2, cells: createPageCells(totalCells, rows, cols) }
509
  ];
510
 
511
- // Reset UI
512
  selectedCellIndex.value = null;
513
  inputBuffer.value = '';
514
  alert("內容已清空!");
515
  }
516
  };
517
 
518
- // Helper to set cell content easily using 1-based Row/Col
519
- // Row: 1-8, Col: 1-6
520
- const setCell = (pageIndex, row, col, type, content, rotation = 0) => {
521
- // Bounds check
522
- if (row > currentGrid.value.rows || col > currentGrid.value.cols) return;
523
-
524
- const cells = pages.value[pageIndex].cells;
525
- const index = (row - 1) * currentGrid.value.cols + (col - 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
 
527
- if (cells[index]) {
528
- cells[index].type = type;
529
- cells[index].content = content;
530
- cells[index].rotation = rotation;
531
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
  };
533
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  const exportProject = () => {
535
  const projectData = {
536
- version: '1.0',
537
  timestamp: new Date().toISOString(),
538
  grid: currentGrid.value,
539
- pages: pages.value
 
540
  };
541
 
542
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(projectData));
543
  const downloadAnchorNode = document.createElement('a');
544
  downloadAnchorNode.setAttribute("href", dataStr);
545
  downloadAnchorNode.setAttribute("download", "magic_origami_project.json");
546
- document.body.appendChild(downloadAnchorNode); // required for firefox
547
  downloadAnchorNode.click();
548
  downloadAnchorNode.remove();
549
  };
@@ -561,16 +697,18 @@
561
  try {
562
  const importedData = JSON.parse(e.target.result);
563
 
564
- // Basic validation
565
  if (!importedData.grid || !importedData.pages) {
566
  throw new Error("Invalid project file format");
567
  }
568
 
569
- // Restore state
570
  currentGrid.value = importedData.grid;
571
  pages.value = importedData.pages;
572
 
573
- // Reset UI
 
 
 
 
574
  selectedCellIndex.value = null;
575
  activePageId.value = 1;
576
  viewMode.value = 'overview';
@@ -580,7 +718,6 @@
580
  console.error(err);
581
  alert("讀取檔案失敗,請確認檔案格式正確。");
582
  } finally {
583
- // Reset input value to allow re-importing same file if needed
584
  event.target.value = '';
585
  }
586
  };
@@ -588,14 +725,11 @@
588
  };
589
 
590
  const applyTemplate = (templateId) => {
591
- // 移除了 confirm() 對話框,直接套用模板
592
  try {
593
- // 1. Force switch to 6x8 grid
594
  const targetConf = gridOptions[1]; // 6x8
595
  currentGrid.value = targetConf;
596
  const totalCells = 48;
597
 
598
- // Reset Pages
599
  pages.value = [
600
  { id: 1, cells: createPageCells(0, 8, 6) },
601
  { id: 2, cells: createPageCells(48, 8, 6) }
@@ -631,7 +765,6 @@
631
  setCell(1, 8, 6, 'text', '海', 180);
632
  }
633
 
634
- // Reset View
635
  selectedCellIndex.value = null;
636
  activePageId.value = 1;
637
  viewMode.value = 'overview';
@@ -644,198 +777,220 @@
644
  }
645
  };
646
 
647
- const switchToPage = (pageId) => {
648
- activePageId.value = pageId;
649
- viewMode.value = 'edit';
650
- selectedCellIndex.value = null;
651
- inputBuffer.value = '';
652
- };
653
 
654
- const handleCellClick = (index) => {
655
- if (selectedCellIndex.value !== index) {
656
- selectedCellIndex.value = index;
657
- const cell = activePageCells.value[index];
658
- inputBuffer.value = (cell.type === 'text') ? cell.content : '';
659
- nextTick(() => { if (textInputRef.value) textInputRef.value.focus(); });
660
- } else {
661
- rotateCurrentCell();
662
- }
663
- };
664
 
665
- const rotateCurrentCell = () => {
666
- if (selectedCellIndex.value === null) return;
667
- const cell = activePageCells.value[selectedCellIndex.value];
668
- cell.rotation = (cell.rotation + 90) % 360;
669
- };
670
 
671
- const updateSelectedCellText = () => {
672
- if (selectedCellIndex.value === null) return;
673
- const cell = activePageCells.value[selectedCellIndex.value];
674
- cell.type = 'text';
675
- cell.content = inputBuffer.value;
676
- };
677
 
678
- const applyIconToCell = (iconName) => {
679
- if (selectedCellIndex.value === null) return;
680
- const cell = activePageCells.value[selectedCellIndex.value];
681
- cell.type = 'icon';
682
- cell.content = iconName;
683
- inputBuffer.value = '';
684
- };
685
 
686
- const clearCurrentCell = () => {
687
- if (selectedCellIndex.value === null) return;
688
- const cell = activePageCells.value[selectedCellIndex.value];
689
- cell.content = '';
690
- cell.type = 'text';
691
- cell.rotation = 0;
692
- inputBuffer.value = '';
693
- };
694
 
695
- const renderPageToCanvas = async (pageId) => {
696
- const pageData = pages.value[pageId - 1];
697
- const rows = currentGrid.value.rows;
698
- const cols = currentGrid.value.cols;
699
 
700
- const container = document.getElementById('pdf-generator-container');
701
- container.innerHTML = '';
702
 
703
- const wrapper = document.createElement('div');
704
- wrapper.style.width = '210mm';
705
- wrapper.style.height = '297mm';
706
- wrapper.style.backgroundColor = 'white';
707
- wrapper.style.position = 'relative';
708
-
709
- let gridHtml = `<div style="display: grid; grid-template-columns: repeat(${cols}, 1fr); grid-template-rows: repeat(${rows}, 1fr); width: 100%; height: 100%; border: 1px solid #e2e8f0;">`;
710
-
711
- pageData.cells.forEach((cell, idx) => {
712
- let contentHtml = '';
713
- if (cell.type === 'text') {
714
- contentHtml = `<span class="export-text" style="font-size: 40px; font-weight: bold; color: #1e293b; font-family: 'Noto Sans TC', sans-serif;">${cell.content}</span>`;
715
- } else if (cell.type === 'icon') {
716
- contentHtml = `<svg viewBox="0 0 24 24" style="width: 60%; height: 60%; fill: #1e293b;"><path d="${icons[cell.content]}"></path></svg>`;
717
- }
 
 
 
 
 
 
718
 
719
- const coord = `${Math.floor(idx / cols) + 1}-${(idx % cols) + 1}`;
720
 
721
- gridHtml += `
722
- <div class="grid-cell" style="position: relative; border: 1px solid #cbd5e1; display: flex; align-items: center; justify-content: center; overflow: hidden;">
723
- <span style="position: absolute; top: 4px; left: 4px; font-size: 8px; color: #e2e8f0;">${coord}</span>
724
- <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; transform: rotate(${cell.rotation}deg);">
725
- ${contentHtml}
726
- </div>
727
  </div>
728
- `;
729
- });
730
- gridHtml += `</div>`;
731
-
732
- gridHtml += `<div style="position: absolute; bottom: 5px; right: 10px; color: #e2e8f0; font-size: 10px; font-family: sans-serif;">Page ${pageId} - Magic Origami</div>`;
733
-
734
- wrapper.innerHTML = gridHtml;
735
- container.appendChild(wrapper);
736
-
737
- const canvas = await html2canvas(wrapper, {
738
- scale: 3,
739
- useCORS: true,
740
- backgroundColor: '#ffffff',
741
- onclone: (clonedDoc) => {
742
- const textElements = clonedDoc.querySelectorAll('.export-text');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
 
744
- textElements.forEach(el => {
745
- const textContent = el.innerText;
746
- if (!textContent) return;
747
-
748
- const ns = "http://www.w3.org/2000/svg";
749
- const svg = document.createElementNS(ns, "svg");
750
- svg.setAttribute("width", "100%");
751
- svg.setAttribute("height", "100%");
752
- svg.setAttribute("viewBox", "0 0 100 100");
753
- svg.style.position = "absolute";
754
- svg.style.top = "0";
755
- svg.style.left = "0";
756
-
757
- const textNode = document.createElementNS(ns, "text");
758
- textNode.setAttribute("x", "50%");
759
- textNode.setAttribute("y", "50%");
760
- textNode.setAttribute("dominant-baseline", "central");
761
- textNode.setAttribute("text-anchor", "middle");
762
- textNode.setAttribute("fill", "#1e293b");
763
- textNode.setAttribute("font-family", "'Noto Sans TC', sans-serif");
764
- textNode.setAttribute("font-weight", "bold");
765
- textNode.setAttribute("font-size", "45");
766
- textNode.textContent = textContent;
767
-
768
- svg.appendChild(textNode);
769
-
770
- const parent = el.parentNode;
771
- parent.style.position = "relative";
772
- parent.innerHTML = '';
773
- parent.appendChild(svg);
774
- });
775
- }
776
- });
777
-
778
- return canvas;
779
- };
780
-
781
- const exportPDF = async () => {
782
- if (selectedCellIndex.value !== null) selectedCellIndex.value = null;
783
- isGenerating.value = true;
784
-
785
- try {
786
- const pdf = new jsPDF('p', 'mm', 'a4');
787
- const pdfWidth = 210;
788
- const pdfHeight = 297;
789
-
790
- const canvas1 = await renderPageToCanvas(1);
791
- const imgData1 = canvas1.toDataURL('image/jpeg', 0.95);
792
- pdf.addImage(imgData1, 'JPEG', 0, 0, pdfWidth, pdfHeight);
793
-
794
- pdf.addPage();
795
- const canvas2 = await renderPageToCanvas(2);
796
- const imgData2 = canvas2.toDataURL('image/jpeg', 0.95);
797
- pdf.addImage(imgData2, 'JPEG', 0, 0, pdfWidth, pdfHeight);
798
 
799
- pdf.save('magic-origami-booklet.pdf');
 
800
 
801
- } catch (err) {
802
- console.error(err);
803
- alert("PDF 生成發生錯誤");
804
- } finally {
805
- isGenerating.value = false;
806
- document.getElementById('pdf-generator-container').innerHTML = '';
807
- }
808
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
 
810
- return {
811
- gridOptions,
812
- currentGrid,
813
- changeGridSize,
814
- applyTemplate,
815
- pages,
816
- viewMode,
817
- activePageId,
818
- activePageCells,
819
- selectedCellIndex,
820
- inputBuffer,
821
- icons,
822
- textInputRef,
823
- isGenerating,
824
- switchToPage,
825
- handleCellClick,
826
- rotateCurrentCell,
827
- updateSelectedCellText,
828
- applyIconToCell,
829
- clearCurrentCell,
830
- clearAllContent, // Export new function
831
- exportPDF,
832
- exportProject,
833
- triggerImport,
834
- importProject,
835
- fileInputRef
836
- };
837
- }
838
- }).mount('#app');
 
 
 
 
 
 
 
 
 
 
 
 
839
  </script>
840
  </body>
841
  </html>
 
15
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
16
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
17
 
18
+ <!-- Cropper.js for Image Cropping -->
19
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css" rel="stylesheet">
20
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
21
+
22
  <!-- Google Fonts -->
23
  <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&display=swap" rel="stylesheet">
24
 
 
69
  left: -9999px;
70
  }
71
 
72
+ /* Custom Scrollbar */
73
+ .custom-scrollbar::-webkit-scrollbar {
74
+ width: 6px;
75
+ }
76
+ .custom-scrollbar::-webkit-scrollbar-track {
77
+ background: #f1f5f9;
78
+ }
79
+ .custom-scrollbar::-webkit-scrollbar-thumb {
80
+ background: #cbd5e1;
81
+ border-radius: 3px;
82
+ }
83
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
84
+ background: #94a3b8;
85
+ }
86
+
87
  .no-scrollbar::-webkit-scrollbar {
88
  display: none;
89
  }
 
91
  -ms-overflow-style: none;
92
  scrollbar-width: none;
93
  }
94
+
95
+ /* Cropper CSS Override */
96
+ .cropper-container {
97
+ width: 100%;
98
+ height: 100%;
99
+ }
100
  </style>
101
  </head>
102
  <body class="h-screen overflow-hidden text-slate-800">
 
106
  <!-- =======================
107
  LEFT PANEL: Controls
108
  ======================= -->
109
+ <div class="w-full md:w-1/3 lg:w-1/4 bg-white border-r border-slate-200 flex flex-col h-full shadow-lg z-20 relative">
110
 
111
  <!-- Header -->
112
  <div class="p-6 border-b border-slate-100 bg-slate-50">
 
139
  </div>
140
 
141
  <!-- Scrollable Content -->
142
+ <div class="flex-1 overflow-y-auto p-6 space-y-8 custom-scrollbar">
143
 
144
  <!-- Grid Settings -->
145
  <div>
 
155
  {{ conf.label }}
156
  </button>
157
  </div>
158
+ <!-- Clear All Button -->
159
  <button
160
  @click="clearAllContent"
161
  class="w-full py-2 text-sm text-red-600 border border-red-200 bg-red-50 hover:bg-red-100 rounded-lg transition-all flex items-center justify-center gap-1"
 
174
  @click="applyTemplate('lucky')"
175
  class="w-full py-3 bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 text-white font-bold rounded-lg shadow-md hover:shadow-lg active:scale-95 transition-all flex items-center justify-center gap-2"
176
  >
177
+ <!-- Updated Clover to Emoji -->
178
+ <span class="text-xl leading-none">🍀</span>
179
+ 套用:幸運遇見你
 
 
180
  </button>
181
  <p class="text-xs text-slate-400 mt-2">提示:套用後會自動切換為 6x8 網格並填入內容。</p>
182
  </div>
183
 
184
+ <!-- Project Management -->
185
  <div class="border-t border-slate-200 pt-4">
186
  <label class="block text-sm font-bold text-slate-700 mb-2">💾 專案管理</label>
187
  <div class="grid grid-cols-2 gap-2">
 
211
  @change="importProject"
212
  >
213
  </div>
 
214
  </div>
215
 
216
  <!-- Instruction -->
 
225
  </div>
226
 
227
  <!-- Editing Controls -->
228
+ <div v-if="viewMode === 'edit'" class="space-y-8 animate-fade-in pb-10">
229
  <!-- Text Input -->
230
  <div>
231
  <label class="block text-sm font-bold text-slate-700 mb-2">1. 文字輸入</label>
 
247
  <label class="block text-sm font-bold text-slate-700 mb-3">2. 選擇圖示</label>
248
  <div class="grid grid-cols-4 gap-2 max-h-60 overflow-y-auto p-1 custom-scrollbar">
249
  <button
250
+ v-for="(iconVal, name) in icons"
251
  :key="name"
252
  @click="applyIconToCell(name)"
253
  :disabled="selectedCellIndex === null"
 
255
  :class="selectedCellIndex === null ? 'border-slate-200 text-slate-300 cursor-not-allowed' : 'border-slate-300 text-slate-600 hover:border-indigo-400 hover:text-indigo-600 cursor-pointer'"
256
  :title="name"
257
  >
258
+ <span class="text-3xl leading-none select-none">{{ iconVal }}</span>
259
+ </button>
260
+ </div>
261
+ </div>
262
+
263
+ <!-- Image Upload -->
264
+ <div>
265
+ <div class="flex justify-between items-center mb-3">
266
+ <label class="block text-sm font-bold text-slate-700">3. 自定義圖片</label>
267
+ <button
268
+ @click="triggerImageUpload"
269
+ 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"
270
+ >
271
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
272
+ <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
273
  </svg>
274
+ 上傳圖片
275
+ </button>
276
+ <input type="file" ref="imageUploadInput" class="hidden" accept="image/*" @change="handleImageUpload">
277
+ </div>
278
+
279
+ <div v-if="customImages.length === 0" class="text-xs text-slate-400 text-center py-2 border border-dashed border-slate-300 rounded-lg">
280
+ 尚未上傳圖片
281
+ </div>
282
+ <div v-else class="grid grid-cols-4 gap-2 p-1">
283
+ <button
284
+ v-for="(imgSrc, idx) in customImages"
285
+ :key="idx"
286
+ @click="applyCustomImageToCell(imgSrc)"
287
+ :disabled="selectedCellIndex === null"
288
+ class="aspect-square flex items-center justify-center rounded-lg border overflow-hidden relative hover:opacity-90 active:scale-95 transition-all bg-white"
289
+ :class="selectedCellIndex === null ? 'border-slate-200 cursor-not-allowed opacity-50' : 'border-slate-300 cursor-pointer hover:border-indigo-400 ring-offset-1'"
290
+ >
291
+ <img :src="imgSrc" class="w-full h-full object-cover">
292
+ <!-- Delete Button -->
293
+ <div @click.stop="removeCustomImage(idx)" class="absolute top-0 right-0 bg-black/50 text-white w-4 h-4 flex items-center justify-center text-[10px] hover:bg-red-500 rounded-bl">×</div>
294
  </button>
295
  </div>
296
  </div>
 
341
  </span>
342
  </button>
343
  </div>
344
+
345
+ <!-- Image Cropper Modal -->
346
+ <div v-if="showCropper" class="absolute inset-0 z-50 bg-slate-900/90 flex flex-col p-4 animate-fade-in">
347
+ <div class="flex-1 relative bg-black rounded-lg overflow-hidden mb-4">
348
+ <img ref="cropperImgRef" :src="tempImageSrc" class="max-w-full max-h-full block">
349
+ </div>
350
+ <div class="flex gap-2 justify-end">
351
+ <button @click="cancelCrop" class="px-4 py-2 text-white text-sm font-bold bg-slate-600 rounded hover:bg-slate-500">取消</button>
352
+ <button @click="confirmCrop" class="px-4 py-2 text-white text-sm font-bold bg-indigo-600 rounded hover:bg-indigo-500">確認裁切</button>
353
+ </div>
354
+ </div>
355
+
356
  </div>
357
 
358
  <!-- =======================
 
371
  style="width: 120mm; height: 170mm;"
372
  @click="switchToPage(pageId)"
373
  >
 
374
  <div
375
  class="grid gap-0 border border-slate-200 w-full h-full pointer-events-none"
376
  :key="currentGrid.label"
 
386
  >
387
  <div class="rotation-wrapper" :style="{ transform: `rotate(${cell.rotation}deg)` }">
388
  <span v-if="cell.type === 'text'" class="text-2xl font-bold text-slate-800 cell-text">{{ cell.content }}</span>
389
+
390
+ <!-- Render Icon: Emoji -->
391
+ <span v-if="cell.type === 'icon'" class="text-3xl leading-none select-none">{{ icons[cell.content] }}</span>
392
+
393
+ <!-- Image Renderer -->
394
+ <img v-if="cell.type === 'image'" :src="cell.content" class="w-4/5 h-4/5 object-contain">
395
  </div>
396
  </div>
397
  </div>
 
414
  Page {{ activePageId }} - {{ currentGrid.rows }}x{{ currentGrid.cols }}
415
  </div>
416
 
 
417
  <div
418
  class="grid gap-0 border border-slate-200 w-full h-full"
419
  :key="currentGrid.label + activePageId"
 
438
  <span v-if="cell.type === 'text'" class="text-4xl md:text-5xl font-bold text-slate-800 cell-text block">
439
  {{ cell.content }}
440
  </span>
441
+
442
+ <!-- Render Icon: Emoji -->
443
+ <span v-if="cell.type === 'icon'" class="text-4xl md:text-5xl leading-none select-none">{{ icons[cell.content] }}</span>
444
+
445
+ <!-- Image Renderer -->
446
+ <img v-if="cell.type === 'image'" :src="cell.content" class="w-4/5 h-4/5 object-contain">
447
  </div>
448
  </div>
449
  </div>
 
469
  </div>
470
 
471
  <script>
472
+ const { createApp, ref, computed, nextTick, onMounted } = Vue;
473
  const { jsPDF } = window.jspdf;
474
 
475
  createApp({
 
482
  ];
483
  const currentGrid = ref(gridOptions[1]); // Default 6x8
484
 
485
+ // --- Icons (Unified to Emoji) ---
486
+ // Using standard unicode emojis for consistent PDF export logic
487
  const icons = {
488
+ '幸運草': '🍀',
489
+ '愛心': '❤️',
490
+ '星星': '⭐',
491
+ '勝利': '✌️',
492
+ '獎盃': '🏆',
493
+ '笑臉': '😊',
494
+ '皇冠': '👑',
495
+ '鑽石': '💎',
496
+ '燈泡': '💡',
497
+ '太陽': '☀️',
498
+ '月亮': '🌙',
499
+ '雲朵': '☁️',
500
+ '音符': '🎵',
501
+ '飛機': '✈️',
502
+ '花朵': '🌸',
503
+ '樹木': '🌳',
504
+ '禮物': '🎁'
505
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
 
507
+ // --- State Initialization ---
508
+ const createPageCells = (pageOffset, rows, cols) => Array.from({ length: rows * cols }, (_, i) => ({
509
+ id: pageOffset + i,
510
+ type: 'text',
511
+ content: '',
512
+ rotation: 0
513
+ }));
514
+
515
+ const pages = ref([
516
+ { id: 1, cells: createPageCells(0, 8, 6) },
517
+ { id: 2, cells: createPageCells(48, 8, 6) }
518
+ ]);
519
+
520
+ const viewMode = ref('overview');
521
+ const activePageId = ref(1);
522
+ const selectedCellIndex = ref(null);
523
+ const inputBuffer = ref('');
524
+ const isGenerating = ref(false);
525
+
526
+ // Refs
527
+ const textInputRef = ref(null);
528
+ const fileInputRef = ref(null);
529
+ const imageUploadInput = ref(null);
530
+ const cropperImgRef = ref(null);
531
 
532
+ // Image Upload & Crop State
533
+ const showCropper = ref(false);
534
+ const tempImageSrc = ref('');
535
+ const customImages = ref([]); // Stores base64 strings
536
+ let cropperInstance = null;
 
 
537
 
538
+ const activePageCells = computed(() => pages.value[activePageId.value - 1].cells);
539
 
540
+ // --- Actions ---
541
 
542
  const changeGridSize = (conf) => {
 
543
  if (currentGrid.value.label === conf.label) return;
544
 
 
545
  const hasContent = pages.value.some(p => p.cells.some(c => c.content !== ''));
 
546
  if (hasContent) {
547
  alert("若要切換網格設定請清除內容後再切換。");
548
  return;
549
  }
550
 
 
551
  currentGrid.value = conf;
552
  const totalCells = conf.rows * conf.cols;
553
 
 
554
  pages.value = [
555
  { id: 1, cells: createPageCells(0, conf.rows, conf.cols) },
556
  { id: 2, cells: createPageCells(totalCells, conf.rows, conf.cols) }
557
  ];
558
 
 
559
  selectedCellIndex.value = null;
560
  activePageId.value = 1;
561
  viewMode.value = 'overview';
 
569
  }
570
 
571
  if (confirm("確定要進行一鍵清空嗎?所有編輯的內容將會消失且無法復原。")) {
 
572
  const rows = currentGrid.value.rows;
573
  const cols = currentGrid.value.cols;
574
  const totalCells = rows * cols;
 
578
  { id: 2, cells: createPageCells(totalCells, rows, cols) }
579
  ];
580
 
 
581
  selectedCellIndex.value = null;
582
  inputBuffer.value = '';
583
  alert("內容已清空!");
584
  }
585
  };
586
 
587
+ const setCell = (pageIndex, row, col, type, content, rotation = 0) => {
588
+ if (row > currentGrid.value.rows || col > currentGrid.value.cols) return;
589
+ const cells = pages.value[pageIndex].cells;
590
+ const index = (row - 1) * currentGrid.value.cols + (col - 1);
591
+ if (cells[index]) {
592
+ cells[index].type = type;
593
+ cells[index].content = content;
594
+ cells[index].rotation = rotation;
595
+ }
596
+ };
597
+
598
+ // --- Image Upload Logic ---
599
+
600
+ const triggerImageUpload = () => {
601
+ imageUploadInput.value.click();
602
+ };
603
+
604
+ const handleImageUpload = (event) => {
605
+ const file = event.target.files[0];
606
+ if (!file) return;
607
+
608
+ const reader = new FileReader();
609
+ reader.onload = (e) => {
610
+ tempImageSrc.value = e.target.result;
611
+ showCropper.value = true;
612
+ // Reset input so same file can be selected again if needed
613
+ event.target.value = '';
614
 
615
+ // Initialize cropper after DOM update
616
+ nextTick(() => {
617
+ if (cropperInstance) cropperInstance.destroy();
618
+ cropperInstance = new Cropper(cropperImgRef.value, {
619
+ aspectRatio: 1,
620
+ viewMode: 1,
621
+ dragMode: 'move',
622
+ autoCropArea: 0.9,
623
+ background: false
624
+ });
625
+ });
626
+ };
627
+ reader.readAsDataURL(file);
628
+ };
629
+
630
+ const cancelCrop = () => {
631
+ showCropper.value = false;
632
+ if (cropperInstance) {
633
+ cropperInstance.destroy();
634
+ cropperInstance = null;
635
+ }
636
+ tempImageSrc.value = '';
637
  };
638
 
639
+ const confirmCrop = () => {
640
+ if (!cropperInstance) return;
641
+
642
+ const canvas = cropperInstance.getCroppedCanvas({
643
+ width: 300, // Resize to 300x300 for optimization
644
+ height: 300
645
+ });
646
+
647
+ const base64 = canvas.toDataURL('image/jpeg', 0.85); // Compress slightly
648
+ customImages.value.push(base64);
649
+
650
+ cancelCrop(); // Cleanup and close
651
+ };
652
+
653
+ const applyCustomImageToCell = (imgSrc) => {
654
+ if (selectedCellIndex.value === null) return;
655
+ const cell = activePageCells.value[selectedCellIndex.value];
656
+ cell.type = 'image';
657
+ cell.content = imgSrc;
658
+ inputBuffer.value = '';
659
+ };
660
+
661
+ const removeCustomImage = (idx) => {
662
+ if(confirm('確定要移除這張自定義圖片嗎?')) {
663
+ customImages.value.splice(idx, 1);
664
+ }
665
+ };
666
+
667
+ // --- Export / Import ---
668
+
669
  const exportProject = () => {
670
  const projectData = {
671
+ version: '1.3',
672
  timestamp: new Date().toISOString(),
673
  grid: currentGrid.value,
674
+ pages: pages.value,
675
+ customImages: customImages.value // Also save custom images
676
  };
677
 
678
  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(projectData));
679
  const downloadAnchorNode = document.createElement('a');
680
  downloadAnchorNode.setAttribute("href", dataStr);
681
  downloadAnchorNode.setAttribute("download", "magic_origami_project.json");
682
+ document.body.appendChild(downloadAnchorNode);
683
  downloadAnchorNode.click();
684
  downloadAnchorNode.remove();
685
  };
 
697
  try {
698
  const importedData = JSON.parse(e.target.result);
699
 
 
700
  if (!importedData.grid || !importedData.pages) {
701
  throw new Error("Invalid project file format");
702
  }
703
 
 
704
  currentGrid.value = importedData.grid;
705
  pages.value = importedData.pages;
706
 
707
+ // Import custom images if they exist
708
+ if (importedData.customImages && Array.isArray(importedData.customImages)) {
709
+ customImages.value = importedData.customImages;
710
+ }
711
+
712
  selectedCellIndex.value = null;
713
  activePageId.value = 1;
714
  viewMode.value = 'overview';
 
718
  console.error(err);
719
  alert("讀取檔案失敗,請確認檔案格式正確。");
720
  } finally {
 
721
  event.target.value = '';
722
  }
723
  };
 
725
  };
726
 
727
  const applyTemplate = (templateId) => {
 
728
  try {
 
729
  const targetConf = gridOptions[1]; // 6x8
730
  currentGrid.value = targetConf;
731
  const totalCells = 48;
732
 
 
733
  pages.value = [
734
  { id: 1, cells: createPageCells(0, 8, 6) },
735
  { id: 2, cells: createPageCells(48, 8, 6) }
 
765
  setCell(1, 8, 6, 'text', '海', 180);
766
  }
767
 
 
768
  selectedCellIndex.value = null;
769
  activePageId.value = 1;
770
  viewMode.value = 'overview';
 
777
  }
778
  };
779
 
780
+ const switchToPage = (pageId) => {
781
+ activePageId.value = pageId;
782
+ viewMode.value = 'edit';
783
+ selectedCellIndex.value = null;
784
+ inputBuffer.value = '';
785
+ };
786
 
787
+ const handleCellClick = (index) => {
788
+ if (selectedCellIndex.value !== index) {
789
+ selectedCellIndex.value = index;
790
+ const cell = activePageCells.value[index];
791
+ inputBuffer.value = (cell.type === 'text') ? cell.content : '';
792
+ nextTick(() => { if (textInputRef.value) textInputRef.value.focus(); });
793
+ } else {
794
+ rotateCurrentCell();
795
+ }
796
+ };
797
 
798
+ const rotateCurrentCell = () => {
799
+ if (selectedCellIndex.value === null) return;
800
+ const cell = activePageCells.value[selectedCellIndex.value];
801
+ cell.rotation = (cell.rotation + 90) % 360;
802
+ };
803
 
804
+ const updateSelectedCellText = () => {
805
+ if (selectedCellIndex.value === null) return;
806
+ const cell = activePageCells.value[selectedCellIndex.value];
807
+ cell.type = 'text';
808
+ cell.content = inputBuffer.value;
809
+ };
810
 
811
+ const applyIconToCell = (iconName) => {
812
+ if (selectedCellIndex.value === null) return;
813
+ const cell = activePageCells.value[selectedCellIndex.value];
814
+ cell.type = 'icon';
815
+ cell.content = iconName;
816
+ inputBuffer.value = '';
817
+ };
818
 
819
+ const clearCurrentCell = () => {
820
+ if (selectedCellIndex.value === null) return;
821
+ const cell = activePageCells.value[selectedCellIndex.value];
822
+ cell.content = '';
823
+ cell.type = 'text';
824
+ cell.rotation = 0;
825
+ inputBuffer.value = '';
826
+ };
827
 
828
+ const renderPageToCanvas = async (pageId) => {
829
+ const pageData = pages.value[pageId - 1];
830
+ const rows = currentGrid.value.rows;
831
+ const cols = currentGrid.value.cols;
832
 
833
+ const container = document.getElementById('pdf-generator-container');
834
+ container.innerHTML = '';
835
 
836
+ const wrapper = document.createElement('div');
837
+ wrapper.style.width = '210mm';
838
+ wrapper.style.height = '297mm';
839
+ wrapper.style.backgroundColor = 'white';
840
+ wrapper.style.position = 'relative';
841
+
842
+ let gridHtml = `<div style="display: grid; grid-template-columns: repeat(${cols}, 1fr); grid-template-rows: repeat(${rows}, 1fr); width: 100%; height: 100%; border: 1px solid #e2e8f0;">`;
843
+
844
+ pageData.cells.forEach((cell, idx) => {
845
+ let contentHtml = '';
846
+ if (cell.type === 'text') {
847
+ contentHtml = `<span class="export-text" style="font-size: 40px; font-weight: bold; color: #1e293b; font-family: 'Noto Sans TC', sans-serif;">${cell.content}</span>`;
848
+ } else if (cell.type === 'icon') {
849
+ // FIX: Treat icons (Emoji) as text for export!
850
+ // Using same class 'export-text' so they get SVG centered replacement.
851
+ // Slightly larger font size for emojis usually looks better.
852
+ const iconVal = icons[cell.content];
853
+ contentHtml = `<span class="export-text" style="font-size: 50px; line-height: 1; user-select: none;">${iconVal}</span>`;
854
+ } else if (cell.type === 'image') {
855
+ contentHtml = `<img src="${cell.content}" style="width: 80%; height: 80%; object-fit: contain;">`;
856
+ }
857
 
858
+ const coord = `${Math.floor(idx / cols) + 1}-${(idx % cols) + 1}`;
859
 
860
+ gridHtml += `
861
+ <div class="grid-cell" style="position: relative; border: 1px solid #cbd5e1; display: flex; align-items: center; justify-content: center; overflow: hidden;">
862
+ <span style="position: absolute; top: 4px; left: 4px; font-size: 8px; color: #e2e8f0;">${coord}</span>
863
+ <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; transform: rotate(${cell.rotation}deg);">
864
+ ${contentHtml}
 
865
  </div>
866
+ </div>
867
+ `;
868
+ });
869
+ gridHtml += `</div>`;
870
+
871
+ gridHtml += `<div style="position: absolute; bottom: 5px; right: 10px; color: #e2e8f0; font-size: 10px; font-family: sans-serif;">Page ${pageId} - Magic Origami</div>`;
872
+
873
+ wrapper.innerHTML = gridHtml;
874
+ container.appendChild(wrapper);
875
+
876
+ const canvas = await html2canvas(wrapper, {
877
+ scale: 3,
878
+ useCORS: true,
879
+ backgroundColor: '#ffffff',
880
+ onclone: (clonedDoc) => {
881
+ // Both text and icons (emojis) now have class 'export-text'
882
+ const textElements = clonedDoc.querySelectorAll('.export-text');
883
+
884
+ textElements.forEach(el => {
885
+ const textContent = el.innerText;
886
+ if (!textContent) return;
887
+
888
+ const ns = "http://www.w3.org/2000/svg";
889
+ const svg = document.createElementNS(ns, "svg");
890
+ svg.setAttribute("width", "100%");
891
+ svg.setAttribute("height", "100%");
892
+ svg.setAttribute("viewBox", "0 0 100 100");
893
+ svg.style.position = "absolute";
894
+ svg.style.top = "0";
895
+ svg.style.left = "0";
896
+
897
+ const textNode = document.createElementNS(ns, "text");
898
+ textNode.setAttribute("x", "50%");
899
+ textNode.setAttribute("y", "50%");
900
+ textNode.setAttribute("dominant-baseline", "central");
901
+ textNode.setAttribute("text-anchor", "middle");
902
+ textNode.setAttribute("fill", "#1e293b");
903
+ textNode.setAttribute("font-family", "'Noto Sans TC', sans-serif"); // Noto Sans supports many emojis
904
+ textNode.setAttribute("font-weight", "bold");
905
 
906
+ // Adjust size slightly for Emojis vs Text if needed,
907
+ // but 45 is a safe middle ground for the SVG viewbox 0-100
908
+ textNode.setAttribute("font-size", "45");
909
+ textNode.textContent = textContent;
910
+
911
+ svg.appendChild(textNode);
912
+
913
+ const parent = el.parentNode;
914
+ parent.style.position = "relative";
915
+ parent.innerHTML = '';
916
+ parent.appendChild(svg);
917
+ });
918
+ }
919
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
 
921
+ return canvas;
922
+ };
923
 
924
+ const exportPDF = async () => {
925
+ if (selectedCellIndex.value !== null) selectedCellIndex.value = null;
926
+ isGenerating.value = true;
927
+
928
+ try {
929
+ const pdf = new jsPDF('p', 'mm', 'a4');
930
+ const pdfWidth = 210;
931
+ const pdfHeight = 297;
932
+
933
+ const canvas1 = await renderPageToCanvas(1);
934
+ const imgData1 = canvas1.toDataURL('image/jpeg', 0.95);
935
+ pdf.addImage(imgData1, 'JPEG', 0, 0, pdfWidth, pdfHeight);
936
+
937
+ pdf.addPage();
938
+ const canvas2 = await renderPageToCanvas(2);
939
+ const imgData2 = canvas2.toDataURL('image/jpeg', 0.95);
940
+ pdf.addImage(imgData2, 'JPEG', 0, 0, pdfWidth, pdfHeight);
941
+
942
+ pdf.save('magic-origami-booklet.pdf');
943
+
944
+ } catch (err) {
945
+ console.error(err);
946
+ alert("PDF 生成發生錯誤");
947
+ } finally {
948
+ isGenerating.value = false;
949
+ document.getElementById('pdf-generator-container').innerHTML = '';
950
+ }
951
+ };
952
 
953
+ return {
954
+ gridOptions,
955
+ currentGrid,
956
+ changeGridSize,
957
+ applyTemplate,
958
+ pages,
959
+ viewMode,
960
+ activePageId,
961
+ activePageCells,
962
+ selectedCellIndex,
963
+ inputBuffer,
964
+ icons,
965
+ textInputRef,
966
+ isGenerating,
967
+ switchToPage,
968
+ handleCellClick,
969
+ rotateCurrentCell,
970
+ updateSelectedCellText,
971
+ applyIconToCell,
972
+ clearCurrentCell,
973
+ clearAllContent,
974
+ exportPDF,
975
+ exportProject,
976
+ triggerImport,
977
+ importProject,
978
+ fileInputRef,
979
+ // Image Upload Refs & Methods
980
+ imageUploadInput,
981
+ cropperImgRef,
982
+ showCropper,
983
+ tempImageSrc,
984
+ customImages,
985
+ triggerImageUpload,
986
+ handleImageUpload,
987
+ cancelCrop,
988
+ confirmCrop,
989
+ applyCustomImageToCell,
990
+ removeCustomImage
991
+ };
992
+ }
993
+ }).mount('#app');
994
  </script>
995
  </body>
996
  </html>