This commit is contained in:
Fatih Kadir Akın 2025-06-17 00:17:25 +03:00
parent d9a42a6bc7
commit a8a69caf76
4 changed files with 2 additions and 520 deletions

View File

@ -31,7 +31,7 @@
<!-- Viewer Mode -->
<div id="viewer-mode" class="viewer-mode h-screen flex flex-col p-2 sm:p-4">
<!-- Context Pills -->
<div id="context-pills" class="flex overflow-x-auto gap-2 empty:hidden mb-0 sm:mb-2 pb-1 scrollbar-hide min-h-[25px]"></div>
<div id="context-pills" class="flex overflow-x-auto gap-2 empty:hidden mb-0 sm:mb-2 pb-1 scrollbar-hide"></div>
<!-- Main Prompt Interface - Full Height -->
<div class="flex-1 flex flex-col">
@ -46,13 +46,6 @@
<!-- Settings Pills -->
<div id="settings-pills" class="flex gap-1 sm:gap-2 flex-wrap flex-1 min-w-0"></div>
<!-- Download Button -->
<button id="download-button" class="w-8 h-8 sm:w-10 sm:h-10 bg-dynamic-muted text-dynamic-foreground rounded-full flex items-center justify-center hover:bg-dynamic-muted/80 transition-colors focus-ring flex-shrink-0 shadow-lg touch-target mr-2" title="Download as PNG">
<svg width="16" height="16" class="sm:w-5 sm:h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/>
</svg>
</button>
<!-- Send Button (circular with arrow up) -->
<button id="copy-button" class="w-8 h-8 sm:w-10 sm:h-10 bg-dynamic-primary text-white rounded-full flex items-center justify-center hover:opacity-90 transition-opacity focus-ring flex-shrink-0 shadow-lg touch-target" title="Send prompt">
<svg width="16" height="16" class="sm:w-5 sm:h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">

View File

@ -177,11 +177,6 @@ class EmbedPreview {
if (copyButton) {
copyButton.addEventListener('click', () => this.handleCopy());
}
const downloadButton = document.getElementById('download-button');
if (downloadButton) {
downloadButton.addEventListener('click', () => this.handleDownload());
}
}
render() {
@ -376,253 +371,6 @@ class EmbedPreview {
}
}
async handleDownload() {
try {
// Get computed styles
const computedStyles = getComputedStyle(document.documentElement);
const bgColor = `rgb(${computedStyles.getPropertyValue('--background').trim()})`;
const fgColor = `rgb(${computedStyles.getPropertyValue('--foreground').trim()})`;
const mutedColor = `rgb(${computedStyles.getPropertyValue('--muted').trim()})`;
const mutedFgColor = `rgb(${computedStyles.getPropertyValue('--muted-foreground').trim()})`;
const primaryColor = `rgb(${computedStyles.getPropertyValue('--primary').trim()})`;
const borderColor = `rgb(${computedStyles.getPropertyValue('--border').trim()})`;
// Create SVG representation
const { svg, scale } = this.createSVG({
bgColor,
fgColor,
mutedColor,
mutedFgColor,
primaryColor,
borderColor
});
// Convert SVG to PNG and download (scale will make it retina-compatible)
await this.downloadSVGasPNG(svg, 'prompt-preview.png', scale);
this.showNotification('Downloaded as PNG!');
} catch (error) {
console.error('Download failed:', error);
this.showNotification('Download failed');
}
}
createSVG(colors) {
const scale = 2; // 2x for retina displays
const width = 800 * scale;
const padding = 16 * scale; // Match sm:p-4 (1rem = 16px)
const mobileGap = 4 * scale; // Match gap-1 on mobile
const desktopGap = 8 * scale; // Match sm:gap-2
// Start calculating height dynamically
let yOffset = padding;
let svgContent = '';
// Context pills area with min-height of 25px
const contextPills = document.getElementById('context-pills');
const contextPillsHeight = 25 * scale; // min-h-[25px]
const contextPillsMargin = 8 * scale; // mb-0 sm:mb-2 (using desktop margin)
if (contextPills && contextPills.children.length > 0) {
let xOffset = padding;
const pillHeight = 20 * scale; // Smaller pills in context area
const pillPadding = 8 * scale;
const fontSize = 10.4 * scale;
for (const pill of contextPills.children) {
const pillText = pill.textContent.trim();
const pillWidth = Math.max(60 * scale, pillText.length * 7 * scale + pillPadding * 2);
// Pill background with proper opacity
const pillY = yOffset + (contextPillsHeight - pillHeight) / 2;
svgContent += `<rect x="${xOffset}" y="${pillY}" width="${pillWidth}" height="${pillHeight}" rx="${10 * scale}" fill="${colors.primaryColor}" fill-opacity="0.1" stroke="${colors.primaryColor}" stroke-opacity="0.3" stroke-width="${scale}"/>`;
// Pill text - fix vertical alignment using dominant-baseline
const textY = pillY + pillHeight / 2;
svgContent += `<text x="${xOffset + pillWidth/2}" y="${textY}" text-anchor="middle" dominant-baseline="middle" fill="${colors.fgColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${fontSize}" font-weight="500">${this.escapeXML(pillText)}</text>`;
xOffset += pillWidth + desktopGap;
}
}
yOffset += contextPillsHeight + contextPillsMargin;
// Main prompt container - calculate needed height
const promptText = this.config.prompt || '← Enter your prompt on designer...';
const isPlaceholder = !this.config.prompt;
const containerPadding = 24 * scale; // p-3 sm:p-6 (using desktop padding)
const lineHeight = 22 * scale; // leading-relaxed
const fontSize = 16 * scale; // text-sm sm:text-base (using desktop size)
// Calculate wrapped text lines
const maxTextWidth = width - padding * 2 - containerPadding * 2;
const lines = this.wrapText(promptText, maxTextWidth / scale, 16); // Use unscaled values for text wrapping
const textHeight = Math.max(100 * scale, lines.length * lineHeight + containerPadding * 2);
// Prompt container background
svgContent += `<rect x="${padding}" y="${yOffset}" width="${width - padding * 2}" height="${textHeight}" rx="${12 * scale}" fill="${colors.mutedColor}" stroke="${colors.borderColor}" stroke-width="${scale}"/>`;
// Prompt text
const textColor = isPlaceholder ? colors.mutedFgColor : colors.fgColor;
const fontStyle = isPlaceholder ? 'italic' : 'normal';
let textY = yOffset + containerPadding + fontSize;
for (const line of lines) {
svgContent += `<text x="${padding + containerPadding}" y="${textY}" fill="${textColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${fontSize}" font-style="${fontStyle}">${this.escapeXML(line)}</text>`;
textY += lineHeight;
}
yOffset += textHeight + desktopGap; // mt-0 sm:mt-2
// Bottom bar with settings pills and buttons
const bottomBarHeight = 40 * scale; // h-10 for buttons
const settingsY = yOffset + (bottomBarHeight - 28 * scale) / 2; // Center pills vertically
let settingsX = padding;
// Mode pill (with background)
if (this.config.mode) {
const modeText = this.capitalizeFirst(this.config.mode);
const modePadding = 12 * scale; // px-3
const modeHeight = 32 * scale; // py-2
const modeWidth = modeText.length * 8 * scale + modePadding * 2;
const modeFontSize = 12 * scale;
svgContent += `<rect x="${settingsX}" y="${settingsY}" width="${modeWidth}" height="${modeHeight}" rx="${16 * scale}" fill="${colors.primaryColor}" fill-opacity="0.1" stroke="${colors.primaryColor}" stroke-opacity="0.2" stroke-width="${scale}"/>`;
svgContent += `<text x="${settingsX + modeWidth/2}" y="${settingsY + modeHeight/2}" text-anchor="middle" dominant-baseline="middle" fill="${colors.fgColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${modeFontSize}" font-weight="500">${modeText}</text>`;
settingsX += modeWidth + desktopGap;
}
// Model pill (text only)
if (this.config.model) {
const modelText = this.config.model;
const modelFontSize = 12 * scale;
svgContent += `<text x="${settingsX + 4 * scale}" y="${yOffset + bottomBarHeight/2}" dominant-baseline="middle" fill="${colors.primaryColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${modelFontSize}">${modelText}</text>`;
settingsX += modelText.length * 7 * scale + 12 * scale;
}
// Thinking pill
if (this.config.thinking) {
const thinkingFontSize = 12 * scale;
svgContent += `<text x="${settingsX}" y="${yOffset + bottomBarHeight/2}" dominant-baseline="middle" fill="${colors.primaryColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${thinkingFontSize}">🧠 Thinking</text>`;
settingsX += 70 * scale;
}
// MAX pill
if (this.config.max) {
const maxFontSize = 12 * scale;
svgContent += `<text x="${settingsX}" y="${yOffset + bottomBarHeight/2}" dominant-baseline="middle" fill="${colors.primaryColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${maxFontSize}">⚡ MAX</text>`;
}
// Watermark on the right
const watermarkText = "prompts.chat/embed";
const watermarkFontSize = 14 * scale;
const watermarkX = width - padding - watermarkText.length * 7 * scale;
const watermarkY = yOffset + bottomBarHeight / 2;
// Add watermark text with subtle styling
svgContent += `<text x="${watermarkX}" y="${watermarkY}" dominant-baseline="middle" fill="${colors.mutedFgColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${watermarkFontSize}" font-weight="400" opacity="0.7">${watermarkText}</text>`;
// Calculate final height
const totalHeight = yOffset + bottomBarHeight + padding;
// Create SVG with calculated height
let svg = `<svg width="${width}" height="${totalHeight}" viewBox="0 0 ${width} ${totalHeight}" xmlns="http://www.w3.org/2000/svg">`;
// Background
svg += `<rect width="${width}" height="${totalHeight}" fill="${colors.bgColor}"/>`;
// Add all content
svg += svgContent;
svg += '</svg>';
return { svg, scale };
}
wrapText(text, maxWidth, fontSize) {
const words = text.split(' ');
const lines = [];
let currentLine = '';
// More accurate character width calculation based on font size
const avgCharWidth = fontSize * 0.55; // Adjusted for typical character width
for (const word of words) {
const testLine = currentLine ? currentLine + ' ' + word : word;
const testWidth = testLine.length * avgCharWidth;
if (testWidth > maxWidth && currentLine) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = testLine;
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines;
}
escapeXML(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
async downloadSVGasPNG(svgString, filename, scale = 1) {
// Create a blob from the SVG string
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
// Create an image element
const img = new Image();
return new Promise((resolve, reject) => {
img.onload = () => {
// Create canvas with the actual pixel dimensions
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
// Enable high quality rendering for retina
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// Draw the image at full resolution
ctx.drawImage(img, 0, 0);
// Convert to PNG and download
canvas.toBlob((blob) => {
if (blob) {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
}
URL.revokeObjectURL(url);
resolve();
}, 'image/png', 1.0); // Maximum quality
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load SVG'));
};
img.src = url;
});
}
showNotification(message) {
const notification = document.getElementById('notification');
if (!notification) return;

View File

@ -31,7 +31,7 @@
<!-- Viewer Mode -->
<div id="viewer-mode" class="viewer-mode h-screen flex flex-col p-2 sm:p-4">
<!-- Context Pills -->
<div id="context-pills" class="flex overflow-x-auto gap-2 empty:hidden mb-0 sm:mb-2 pb-1 scrollbar-hide min-h-[25px]"></div>
<div id="context-pills" class="flex overflow-x-auto gap-2 empty:hidden mb-0 sm:mb-2 pb-1 scrollbar-hide"></div>
<!-- Main Prompt Interface - Full Height -->
<div class="flex-1 flex flex-col">
@ -46,13 +46,6 @@
<!-- Settings Pills -->
<div id="settings-pills" class="flex gap-1 sm:gap-2 flex-wrap flex-1 min-w-0"></div>
<!-- Download Button -->
<button id="download-button" class="w-8 h-8 sm:w-10 sm:h-10 bg-dynamic-muted text-dynamic-foreground rounded-full flex items-center justify-center hover:bg-dynamic-muted/80 transition-colors focus-ring flex-shrink-0 shadow-lg touch-target mr-2" title="Download as PNG">
<svg width="16" height="16" class="sm:w-5 sm:h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/>
</svg>
</button>
<!-- Send Button (circular with arrow up) -->
<button id="copy-button" class="w-8 h-8 sm:w-10 sm:h-10 bg-dynamic-primary text-white rounded-full flex items-center justify-center hover:opacity-90 transition-opacity focus-ring flex-shrink-0 shadow-lg touch-target" title="Send prompt">
<svg width="16" height="16" class="sm:w-5 sm:h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">

View File

@ -177,11 +177,6 @@ class EmbedPreview {
if (copyButton) {
copyButton.addEventListener('click', () => this.handleCopy());
}
const downloadButton = document.getElementById('download-button');
if (downloadButton) {
downloadButton.addEventListener('click', () => this.handleDownload());
}
}
render() {
@ -376,253 +371,6 @@ class EmbedPreview {
}
}
async handleDownload() {
try {
// Get computed styles
const computedStyles = getComputedStyle(document.documentElement);
const bgColor = `rgb(${computedStyles.getPropertyValue('--background').trim()})`;
const fgColor = `rgb(${computedStyles.getPropertyValue('--foreground').trim()})`;
const mutedColor = `rgb(${computedStyles.getPropertyValue('--muted').trim()})`;
const mutedFgColor = `rgb(${computedStyles.getPropertyValue('--muted-foreground').trim()})`;
const primaryColor = `rgb(${computedStyles.getPropertyValue('--primary').trim()})`;
const borderColor = `rgb(${computedStyles.getPropertyValue('--border').trim()})`;
// Create SVG representation
const { svg, scale } = this.createSVG({
bgColor,
fgColor,
mutedColor,
mutedFgColor,
primaryColor,
borderColor
});
// Convert SVG to PNG and download (scale will make it retina-compatible)
await this.downloadSVGasPNG(svg, 'prompt-preview.png', scale);
this.showNotification('Downloaded as PNG!');
} catch (error) {
console.error('Download failed:', error);
this.showNotification('Download failed');
}
}
createSVG(colors) {
const scale = 2; // 2x for retina displays
const width = 800 * scale;
const padding = 16 * scale; // Match sm:p-4 (1rem = 16px)
const mobileGap = 4 * scale; // Match gap-1 on mobile
const desktopGap = 8 * scale; // Match sm:gap-2
// Start calculating height dynamically
let yOffset = padding;
let svgContent = '';
// Context pills area with min-height of 25px
const contextPills = document.getElementById('context-pills');
const contextPillsHeight = 25 * scale; // min-h-[25px]
const contextPillsMargin = 8 * scale; // mb-0 sm:mb-2 (using desktop margin)
if (contextPills && contextPills.children.length > 0) {
let xOffset = padding;
const pillHeight = 20 * scale; // Smaller pills in context area
const pillPadding = 8 * scale;
const fontSize = 10.4 * scale;
for (const pill of contextPills.children) {
const pillText = pill.textContent.trim();
const pillWidth = Math.max(60 * scale, pillText.length * 7 * scale + pillPadding * 2);
// Pill background with proper opacity
const pillY = yOffset + (contextPillsHeight - pillHeight) / 2;
svgContent += `<rect x="${xOffset}" y="${pillY}" width="${pillWidth}" height="${pillHeight}" rx="${10 * scale}" fill="${colors.primaryColor}" fill-opacity="0.1" stroke="${colors.primaryColor}" stroke-opacity="0.3" stroke-width="${scale}"/>`;
// Pill text - fix vertical alignment using dominant-baseline
const textY = pillY + pillHeight / 2;
svgContent += `<text x="${xOffset + pillWidth/2}" y="${textY}" text-anchor="middle" dominant-baseline="middle" fill="${colors.fgColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${fontSize}" font-weight="500">${this.escapeXML(pillText)}</text>`;
xOffset += pillWidth + desktopGap;
}
}
yOffset += contextPillsHeight + contextPillsMargin;
// Main prompt container - calculate needed height
const promptText = this.config.prompt || '← Enter your prompt on designer...';
const isPlaceholder = !this.config.prompt;
const containerPadding = 24 * scale; // p-3 sm:p-6 (using desktop padding)
const lineHeight = 22 * scale; // leading-relaxed
const fontSize = 16 * scale; // text-sm sm:text-base (using desktop size)
// Calculate wrapped text lines
const maxTextWidth = width - padding * 2 - containerPadding * 2;
const lines = this.wrapText(promptText, maxTextWidth / scale, 16); // Use unscaled values for text wrapping
const textHeight = Math.max(100 * scale, lines.length * lineHeight + containerPadding * 2);
// Prompt container background
svgContent += `<rect x="${padding}" y="${yOffset}" width="${width - padding * 2}" height="${textHeight}" rx="${12 * scale}" fill="${colors.mutedColor}" stroke="${colors.borderColor}" stroke-width="${scale}"/>`;
// Prompt text
const textColor = isPlaceholder ? colors.mutedFgColor : colors.fgColor;
const fontStyle = isPlaceholder ? 'italic' : 'normal';
let textY = yOffset + containerPadding + fontSize;
for (const line of lines) {
svgContent += `<text x="${padding + containerPadding}" y="${textY}" fill="${textColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${fontSize}" font-style="${fontStyle}">${this.escapeXML(line)}</text>`;
textY += lineHeight;
}
yOffset += textHeight + desktopGap; // mt-0 sm:mt-2
// Bottom bar with settings pills and buttons
const bottomBarHeight = 40 * scale; // h-10 for buttons
const settingsY = yOffset + (bottomBarHeight - 28 * scale) / 2; // Center pills vertically
let settingsX = padding;
// Mode pill (with background)
if (this.config.mode) {
const modeText = this.capitalizeFirst(this.config.mode);
const modePadding = 12 * scale; // px-3
const modeHeight = 32 * scale; // py-2
const modeWidth = modeText.length * 8 * scale + modePadding * 2;
const modeFontSize = 12 * scale;
svgContent += `<rect x="${settingsX}" y="${settingsY}" width="${modeWidth}" height="${modeHeight}" rx="${16 * scale}" fill="${colors.primaryColor}" fill-opacity="0.1" stroke="${colors.primaryColor}" stroke-opacity="0.2" stroke-width="${scale}"/>`;
svgContent += `<text x="${settingsX + modeWidth/2}" y="${settingsY + modeHeight/2}" text-anchor="middle" dominant-baseline="middle" fill="${colors.fgColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${modeFontSize}" font-weight="500">${modeText}</text>`;
settingsX += modeWidth + desktopGap;
}
// Model pill (text only)
if (this.config.model) {
const modelText = this.config.model;
const modelFontSize = 12 * scale;
svgContent += `<text x="${settingsX + 4 * scale}" y="${yOffset + bottomBarHeight/2}" dominant-baseline="middle" fill="${colors.primaryColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${modelFontSize}">${modelText}</text>`;
settingsX += modelText.length * 7 * scale + 12 * scale;
}
// Thinking pill
if (this.config.thinking) {
const thinkingFontSize = 12 * scale;
svgContent += `<text x="${settingsX}" y="${yOffset + bottomBarHeight/2}" dominant-baseline="middle" fill="${colors.primaryColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${thinkingFontSize}">🧠 Thinking</text>`;
settingsX += 70 * scale;
}
// MAX pill
if (this.config.max) {
const maxFontSize = 12 * scale;
svgContent += `<text x="${settingsX}" y="${yOffset + bottomBarHeight/2}" dominant-baseline="middle" fill="${colors.primaryColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${maxFontSize}">⚡ MAX</text>`;
}
// Watermark on the right
const watermarkText = "prompts.chat/embed";
const watermarkFontSize = 14 * scale;
const watermarkX = width - padding - watermarkText.length * 7 * scale;
const watermarkY = yOffset + bottomBarHeight / 2;
// Add watermark text with subtle styling
svgContent += `<text x="${watermarkX}" y="${watermarkY}" dominant-baseline="middle" fill="${colors.mutedFgColor}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif" font-size="${watermarkFontSize}" font-weight="400" opacity="0.7">${watermarkText}</text>`;
// Calculate final height
const totalHeight = yOffset + bottomBarHeight + padding;
// Create SVG with calculated height
let svg = `<svg width="${width}" height="${totalHeight}" viewBox="0 0 ${width} ${totalHeight}" xmlns="http://www.w3.org/2000/svg">`;
// Background
svg += `<rect width="${width}" height="${totalHeight}" fill="${colors.bgColor}"/>`;
// Add all content
svg += svgContent;
svg += '</svg>';
return { svg, scale };
}
wrapText(text, maxWidth, fontSize) {
const words = text.split(' ');
const lines = [];
let currentLine = '';
// More accurate character width calculation based on font size
const avgCharWidth = fontSize * 0.55; // Adjusted for typical character width
for (const word of words) {
const testLine = currentLine ? currentLine + ' ' + word : word;
const testWidth = testLine.length * avgCharWidth;
if (testWidth > maxWidth && currentLine) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = testLine;
}
}
if (currentLine) {
lines.push(currentLine);
}
return lines;
}
escapeXML(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
async downloadSVGasPNG(svgString, filename, scale = 1) {
// Create a blob from the SVG string
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
// Create an image element
const img = new Image();
return new Promise((resolve, reject) => {
img.onload = () => {
// Create canvas with the actual pixel dimensions
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
// Enable high quality rendering for retina
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
// Draw the image at full resolution
ctx.drawImage(img, 0, 0);
// Convert to PNG and download
canvas.toBlob((blob) => {
if (blob) {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
}
URL.revokeObjectURL(url);
resolve();
}, 'image/png', 1.0); // Maximum quality
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load SVG'));
};
img.src = url;
});
}
showNotification(message) {
const notification = document.getElementById('notification');
if (!notification) return;