Update index.html
Browse files- index.html +62 -41
index.html
CHANGED
|
@@ -446,11 +446,6 @@
|
|
| 446 |
class="grid-cell relative border border-slate-300 cursor-pointer hover:bg-slate-50 select-none"
|
| 447 |
:class="{ 'ring-4 ring-indigo-500 ring-inset z-20': selectedCellIndex === index }"
|
| 448 |
>
|
| 449 |
-
<!-- Grid Coordinate -->
|
| 450 |
-
<span class="absolute top-1 left-1 text-[8px] text-slate-200 pointer-events-none z-10">
|
| 451 |
-
{{ Math.floor(index / currentGrid.cols) + 1 }}-{{ (index % currentGrid.cols) + 1 }}
|
| 452 |
-
</span>
|
| 453 |
-
|
| 454 |
<div class="rotation-wrapper pointer-events-none" :style="{ transform: `rotate(${cell.rotation}deg)` }">
|
| 455 |
<span v-if="cell.type === 'text'" class="text-4xl md:text-5xl font-bold cell-text block" :style="{ color: cell.color }">
|
| 456 |
{{ cell.content }}
|
|
@@ -908,6 +903,52 @@
|
|
| 908 |
return content.replace(/['"]/g, '');
|
| 909 |
};
|
| 910 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
// ... PDF Export Logic ...
|
| 912 |
const renderPageToCanvas = async (pageId) => {
|
| 913 |
const pageData = pages.value[pageId - 1];
|
|
@@ -931,7 +972,7 @@
|
|
| 931 |
// Text uses the SVG text replacement trick for perfect centering
|
| 932 |
contentHtml = `<span class="export-text" data-color="${cell.color}" style="font-size: 40px; font-weight: bold; color: ${cell.color}; font-family: 'Noto Sans TC', sans-serif;">${cell.content}</span>`;
|
| 933 |
} else if (cell.type === 'icon') {
|
| 934 |
-
// FontAwesome Icons -> Convert to Unicode Char
|
| 935 |
// This ensures the icon behaves exactly like text (perfectly centered, rotates well)
|
| 936 |
const iconClass = icons[cell.content];
|
| 937 |
const unicode = getIconUnicode(iconClass);
|
|
@@ -941,11 +982,10 @@
|
|
| 941 |
contentHtml = `<img src="${cell.content}" style="width: 80%; height: 80%; object-fit: contain;">`;
|
| 942 |
}
|
| 943 |
|
| 944 |
-
|
| 945 |
|
| 946 |
gridHtml += `
|
| 947 |
<div class="grid-cell" style="position: relative; border: 1px solid #cbd5e1; display: flex; align-items: center; justify-content: center; overflow: hidden;">
|
| 948 |
-
<span style="position: absolute; top: 4px; left: 4px; font-size: 8px; color: #e2e8f0;">${coord}</span>
|
| 949 |
<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; transform: rotate(${cell.rotation}deg);">
|
| 950 |
${contentHtml}
|
| 951 |
</div>
|
|
@@ -973,14 +1013,25 @@
|
|
| 973 |
replaceWithSvgText(el, textContent, color, "'Noto Sans TC', sans-serif", "bold");
|
| 974 |
});
|
| 975 |
|
| 976 |
-
// Handle Icon Centering (
|
| 977 |
const iconElements = clonedDoc.querySelectorAll('.export-icon');
|
| 978 |
iconElements.forEach(el => {
|
| 979 |
const textContent = el.innerText;
|
| 980 |
const color = el.getAttribute('data-color');
|
| 981 |
if (!textContent) return;
|
| 982 |
-
|
| 983 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 984 |
});
|
| 985 |
}
|
| 986 |
});
|
|
@@ -988,36 +1039,6 @@
|
|
| 988 |
return canvas;
|
| 989 |
};
|
| 990 |
|
| 991 |
-
// Helper to replace text element with SVG for perfect centering
|
| 992 |
-
const replaceWithSvgText = (el, content, color, fontFamily, fontWeight) => {
|
| 993 |
-
const ns = "http://www.w3.org/2000/svg";
|
| 994 |
-
const svg = document.createElementNS(ns, "svg");
|
| 995 |
-
svg.setAttribute("width", "100%");
|
| 996 |
-
svg.setAttribute("height", "100%");
|
| 997 |
-
svg.setAttribute("viewBox", "0 0 100 100");
|
| 998 |
-
svg.style.position = "absolute";
|
| 999 |
-
svg.style.top = "0";
|
| 1000 |
-
svg.style.left = "0";
|
| 1001 |
-
|
| 1002 |
-
const textNode = document.createElementNS(ns, "text");
|
| 1003 |
-
textNode.setAttribute("x", "50%");
|
| 1004 |
-
textNode.setAttribute("y", "50%");
|
| 1005 |
-
textNode.setAttribute("dominant-baseline", "central");
|
| 1006 |
-
textNode.setAttribute("text-anchor", "middle");
|
| 1007 |
-
textNode.setAttribute("fill", color);
|
| 1008 |
-
textNode.setAttribute("font-family", fontFamily);
|
| 1009 |
-
textNode.setAttribute("font-weight", fontWeight);
|
| 1010 |
-
textNode.setAttribute("font-size", "45");
|
| 1011 |
-
textNode.textContent = content;
|
| 1012 |
-
|
| 1013 |
-
svg.appendChild(textNode);
|
| 1014 |
-
|
| 1015 |
-
const parent = el.parentNode;
|
| 1016 |
-
parent.style.position = "relative";
|
| 1017 |
-
parent.innerHTML = '';
|
| 1018 |
-
parent.appendChild(svg);
|
| 1019 |
-
};
|
| 1020 |
-
|
| 1021 |
const exportPDF = async () => {
|
| 1022 |
if (selectedCellIndex.value !== null) selectedCellIndex.value = null;
|
| 1023 |
isGenerating.value = true;
|
|
|
|
| 446 |
class="grid-cell relative border border-slate-300 cursor-pointer hover:bg-slate-50 select-none"
|
| 447 |
:class="{ 'ring-4 ring-indigo-500 ring-inset z-20': selectedCellIndex === index }"
|
| 448 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
<div class="rotation-wrapper pointer-events-none" :style="{ transform: `rotate(${cell.rotation}deg)` }">
|
| 450 |
<span v-if="cell.type === 'text'" class="text-4xl md:text-5xl font-bold cell-text block" :style="{ color: cell.color }">
|
| 451 |
{{ cell.content }}
|
|
|
|
| 903 |
return content.replace(/['"]/g, '');
|
| 904 |
};
|
| 905 |
|
| 906 |
+
// Helper to convert text (or icon) to image data URL
|
| 907 |
+
const textToImage = (text, color, font) => {
|
| 908 |
+
const canvas = document.createElement('canvas');
|
| 909 |
+
// Increase resolution for print quality
|
| 910 |
+
canvas.width = 150;
|
| 911 |
+
canvas.height = 150;
|
| 912 |
+
const ctx = canvas.getContext('2d');
|
| 913 |
+
ctx.font = font;
|
| 914 |
+
ctx.fillStyle = color;
|
| 915 |
+
ctx.textAlign = 'center';
|
| 916 |
+
ctx.textBaseline = 'middle';
|
| 917 |
+
// Draw
|
| 918 |
+
ctx.fillText(text, 75, 75);
|
| 919 |
+
return canvas.toDataURL('image/png');
|
| 920 |
+
};
|
| 921 |
+
|
| 922 |
+
// Helper to replace text element with SVG for perfect centering
|
| 923 |
+
const replaceWithSvgText = (el, content, color, fontFamily, fontWeight) => {
|
| 924 |
+
const ns = "http://www.w3.org/2000/svg";
|
| 925 |
+
const svg = document.createElementNS(ns, "svg");
|
| 926 |
+
svg.setAttribute("width", "100%");
|
| 927 |
+
svg.setAttribute("height", "100%");
|
| 928 |
+
svg.setAttribute("viewBox", "0 0 100 100");
|
| 929 |
+
svg.style.position = "absolute";
|
| 930 |
+
svg.style.top = "0";
|
| 931 |
+
svg.style.left = "0";
|
| 932 |
+
|
| 933 |
+
const textNode = document.createElementNS(ns, "text");
|
| 934 |
+
textNode.setAttribute("x", "50%");
|
| 935 |
+
textNode.setAttribute("y", "50%");
|
| 936 |
+
textNode.setAttribute("dominant-baseline", "central");
|
| 937 |
+
textNode.setAttribute("text-anchor", "middle");
|
| 938 |
+
textNode.setAttribute("fill", color);
|
| 939 |
+
textNode.setAttribute("font-family", fontFamily);
|
| 940 |
+
textNode.setAttribute("font-weight", fontWeight);
|
| 941 |
+
textNode.setAttribute("font-size", "45");
|
| 942 |
+
textNode.textContent = content;
|
| 943 |
+
|
| 944 |
+
svg.appendChild(textNode);
|
| 945 |
+
|
| 946 |
+
const parent = el.parentNode;
|
| 947 |
+
parent.style.position = "relative";
|
| 948 |
+
parent.innerHTML = '';
|
| 949 |
+
parent.appendChild(svg);
|
| 950 |
+
};
|
| 951 |
+
|
| 952 |
// ... PDF Export Logic ...
|
| 953 |
const renderPageToCanvas = async (pageId) => {
|
| 954 |
const pageData = pages.value[pageId - 1];
|
|
|
|
| 972 |
// Text uses the SVG text replacement trick for perfect centering
|
| 973 |
contentHtml = `<span class="export-text" data-color="${cell.color}" style="font-size: 40px; font-weight: bold; color: ${cell.color}; font-family: 'Noto Sans TC', sans-serif;">${cell.content}</span>`;
|
| 974 |
} else if (cell.type === 'icon') {
|
| 975 |
+
// FontAwesome Icons -> Convert to Unicode Char
|
| 976 |
// This ensures the icon behaves exactly like text (perfectly centered, rotates well)
|
| 977 |
const iconClass = icons[cell.content];
|
| 978 |
const unicode = getIconUnicode(iconClass);
|
|
|
|
| 982 |
contentHtml = `<img src="${cell.content}" style="width: 80%; height: 80%; object-fit: contain;">`;
|
| 983 |
}
|
| 984 |
|
| 985 |
+
// Coordinate span REMOVED here
|
| 986 |
|
| 987 |
gridHtml += `
|
| 988 |
<div class="grid-cell" style="position: relative; border: 1px solid #cbd5e1; display: flex; align-items: center; justify-content: center; overflow: hidden;">
|
|
|
|
| 989 |
<div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; transform: rotate(${cell.rotation}deg);">
|
| 990 |
${contentHtml}
|
| 991 |
</div>
|
|
|
|
| 1013 |
replaceWithSvgText(el, textContent, color, "'Noto Sans TC', sans-serif", "bold");
|
| 1014 |
});
|
| 1015 |
|
| 1016 |
+
// Handle Icon Centering via Image conversion (Fixes PDF box issue)
|
| 1017 |
const iconElements = clonedDoc.querySelectorAll('.export-icon');
|
| 1018 |
iconElements.forEach(el => {
|
| 1019 |
const textContent = el.innerText;
|
| 1020 |
const color = el.getAttribute('data-color');
|
| 1021 |
if (!textContent) return;
|
| 1022 |
+
|
| 1023 |
+
// Convert to image using canvas in main thread context
|
| 1024 |
+
// 'Font Awesome 6 Free' must be loaded in the page
|
| 1025 |
+
const imgData = textToImage(textContent, color, "900 100px 'Font Awesome 6 Free'");
|
| 1026 |
+
|
| 1027 |
+
const img = document.createElement('img');
|
| 1028 |
+
img.src = imgData;
|
| 1029 |
+
img.style.width = '100%';
|
| 1030 |
+
img.style.height = '100%';
|
| 1031 |
+
img.style.objectFit = 'contain';
|
| 1032 |
+
|
| 1033 |
+
el.parentNode.innerHTML = ''; // Clear parent
|
| 1034 |
+
el.parentNode.appendChild(img);
|
| 1035 |
});
|
| 1036 |
}
|
| 1037 |
});
|
|
|
|
| 1039 |
return canvas;
|
| 1040 |
};
|
| 1041 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1042 |
const exportPDF = async () => {
|
| 1043 |
if (selectedCellIndex.value !== null) selectedCellIndex.value = null;
|
| 1044 |
isGenerating.value = true;
|