diff --git a/static/script/share.js b/static/script/share.js index 5a12b58..ed884ed 100644 --- a/static/script/share.js +++ b/static/script/share.js @@ -6,6 +6,7 @@ async function imageWithTextBlob(imageUrl, text, options = {}) { if (!response.ok) throw new Error(`HTTP ${response.status}`); const imageBlob = await response.blob(); const imageBitmap = await createImageBitmap(imageBlob); + // OffscreenCanvas (works in modern browsers; fallback to normal canvas if needed) const canvas = typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas( @@ -13,16 +14,21 @@ async function imageWithTextBlob(imageUrl, text, options = {}) { options.height ?? imageBitmap.height ) : document.createElement('canvas'); + const width = canvas.width ?? (options.width ?? imageBitmap.width); const height = canvas.height ?? (options.height ?? imageBitmap.height); + if (!(canvas instanceof OffscreenCanvas)) { canvas.width = width; canvas.height = height; } + const ctx = canvas.getContext('2d', { alpha: true }); + // Draw image ctx.drawImage(imageBitmap, 0, 0, width, height); - // No text: return original bitmap as blob (rendered) + + // No text: return original bitmap as blob if (!text?.trim()) { if (canvas.convertToBlob) { return canvas.convertToBlob({ @@ -34,17 +40,20 @@ async function imageWithTextBlob(imageUrl, text, options = {}) { canvas.toBlob((b) => resolve(b), options.type ?? 'image/png', options.quality ?? 0.92) ); } + // === Text rendering === - const basePadding = 6; + const basePadding = options.basePadding ?? 8; // internal left/right/top padding const fontSize = options.fontSize ?? Math.round(width * 0.05); ctx.font = `${fontSize}px ${options.fontFamily ?? 'sans-serif'}`; ctx.textBaseline = 'bottom'; ctx.textAlign = 'center'; + const textColor = options.textColor ?? '#5F1798'; const bgColor = options.bgColor ?? '#fff'; const strokeColor = options.strokeColor ?? 'rgba(0,0,0,0.35)'; const shadowBlur = options.shadowBlur ?? 6; const maxWidth = width - basePadding * 2 - (options.extraPadding ?? 0); + // Wrap text into lines const words = text.split(' '); let line = ''; @@ -59,14 +68,23 @@ async function imageWithTextBlob(imageUrl, text, options = {}) { } } if (line) lines.push(line); + const lineHeight = fontSize + (options.lineGap ?? 6); - const boxHeight = lines.length * lineHeight + basePadding * 2; + + // Extra internal bottom padding inside the rounded box + const internalBottomPadding = options.internalBottomPadding ?? Math.round(fontSize * 0.35); + const boxHeight = lines.length * lineHeight + basePadding + internalBottomPadding; + const maxLineWidth = Math.max(...lines.map((l) => ctx.measureText(l).width)); const boxWidth = maxLineWidth + basePadding * 2; - const bottomPadding = options.bottomPadding ?? Math.round(fontSize * 0.6); + + // Distance from the very bottom of the image to the bottom of the text box + const bottomPadding = options.bottomPadding ?? Math.round(fontSize * 0.9); + const boxX = (width - boxWidth) / 2; const boxY = height - bottomPadding - boxHeight; const radius = options.radius ?? 8; + // Background rounded rect ctx.save(); ctx.fillStyle = bgColor; @@ -74,9 +92,11 @@ async function imageWithTextBlob(imageUrl, text, options = {}) { ctx.shadowBlur = shadowBlur; roundRect(ctx, boxX, boxY, boxWidth, boxHeight, radius, true, false); ctx.restore(); + // Text ctx.fillStyle = textColor; ctx.shadowColor = 'transparent'; + for (let i = 0; i < lines.length; i++) { const lineY = boxY + basePadding + (i + 1) * lineHeight - (lineHeight - fontSize) / 2; const lineX = width / 2; @@ -88,6 +108,7 @@ async function imageWithTextBlob(imageUrl, text, options = {}) { } ctx.fillText(lines[i], lineX, lineY); } + // Convert to blob if (canvas.convertToBlob) { return await canvas.convertToBlob({