feat
This commit is contained in:
parent
8e648752ee
commit
b263e27491
@ -6,6 +6,7 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
|
|||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
const imageBlob = await response.blob();
|
const imageBlob = await response.blob();
|
||||||
const imageBitmap = await createImageBitmap(imageBlob);
|
const imageBitmap = await createImageBitmap(imageBlob);
|
||||||
|
|
||||||
// OffscreenCanvas (works in modern browsers; fallback to normal canvas if needed)
|
// OffscreenCanvas (works in modern browsers; fallback to normal canvas if needed)
|
||||||
const canvas = typeof OffscreenCanvas !== 'undefined'
|
const canvas = typeof OffscreenCanvas !== 'undefined'
|
||||||
? new OffscreenCanvas(
|
? new OffscreenCanvas(
|
||||||
@ -13,16 +14,21 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
|
|||||||
options.height ?? imageBitmap.height
|
options.height ?? imageBitmap.height
|
||||||
)
|
)
|
||||||
: document.createElement('canvas');
|
: document.createElement('canvas');
|
||||||
|
|
||||||
const width = canvas.width ?? (options.width ?? imageBitmap.width);
|
const width = canvas.width ?? (options.width ?? imageBitmap.width);
|
||||||
const height = canvas.height ?? (options.height ?? imageBitmap.height);
|
const height = canvas.height ?? (options.height ?? imageBitmap.height);
|
||||||
|
|
||||||
if (!(canvas instanceof OffscreenCanvas)) {
|
if (!(canvas instanceof OffscreenCanvas)) {
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d', { alpha: true });
|
const ctx = canvas.getContext('2d', { alpha: true });
|
||||||
|
|
||||||
// Draw image
|
// Draw image
|
||||||
ctx.drawImage(imageBitmap, 0, 0, width, height);
|
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 (!text?.trim()) {
|
||||||
if (canvas.convertToBlob) {
|
if (canvas.convertToBlob) {
|
||||||
return 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)
|
canvas.toBlob((b) => resolve(b), options.type ?? 'image/png', options.quality ?? 0.92)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Text rendering ===
|
// === Text rendering ===
|
||||||
const basePadding = 6;
|
const basePadding = options.basePadding ?? 8; // internal left/right/top padding
|
||||||
const fontSize = options.fontSize ?? Math.round(width * 0.05);
|
const fontSize = options.fontSize ?? Math.round(width * 0.05);
|
||||||
ctx.font = `${fontSize}px ${options.fontFamily ?? 'sans-serif'}`;
|
ctx.font = `${fontSize}px ${options.fontFamily ?? 'sans-serif'}`;
|
||||||
ctx.textBaseline = 'bottom';
|
ctx.textBaseline = 'bottom';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
|
|
||||||
const textColor = options.textColor ?? '#5F1798';
|
const textColor = options.textColor ?? '#5F1798';
|
||||||
const bgColor = options.bgColor ?? '#fff';
|
const bgColor = options.bgColor ?? '#fff';
|
||||||
const strokeColor = options.strokeColor ?? 'rgba(0,0,0,0.35)';
|
const strokeColor = options.strokeColor ?? 'rgba(0,0,0,0.35)';
|
||||||
const shadowBlur = options.shadowBlur ?? 6;
|
const shadowBlur = options.shadowBlur ?? 6;
|
||||||
const maxWidth = width - basePadding * 2 - (options.extraPadding ?? 0);
|
const maxWidth = width - basePadding * 2 - (options.extraPadding ?? 0);
|
||||||
|
|
||||||
// Wrap text into lines
|
// Wrap text into lines
|
||||||
const words = text.split(' ');
|
const words = text.split(' ');
|
||||||
let line = '';
|
let line = '';
|
||||||
@ -59,14 +68,23 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (line) lines.push(line);
|
if (line) lines.push(line);
|
||||||
|
|
||||||
const lineHeight = fontSize + (options.lineGap ?? 6);
|
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 maxLineWidth = Math.max(...lines.map((l) => ctx.measureText(l).width));
|
||||||
const boxWidth = maxLineWidth + basePadding * 2;
|
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 boxX = (width - boxWidth) / 2;
|
||||||
const boxY = height - bottomPadding - boxHeight;
|
const boxY = height - bottomPadding - boxHeight;
|
||||||
const radius = options.radius ?? 8;
|
const radius = options.radius ?? 8;
|
||||||
|
|
||||||
// Background rounded rect
|
// Background rounded rect
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.fillStyle = bgColor;
|
ctx.fillStyle = bgColor;
|
||||||
@ -74,9 +92,11 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
|
|||||||
ctx.shadowBlur = shadowBlur;
|
ctx.shadowBlur = shadowBlur;
|
||||||
roundRect(ctx, boxX, boxY, boxWidth, boxHeight, radius, true, false);
|
roundRect(ctx, boxX, boxY, boxWidth, boxHeight, radius, true, false);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
ctx.fillStyle = textColor;
|
ctx.fillStyle = textColor;
|
||||||
ctx.shadowColor = 'transparent';
|
ctx.shadowColor = 'transparent';
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const lineY = boxY + basePadding + (i + 1) * lineHeight - (lineHeight - fontSize) / 2;
|
const lineY = boxY + basePadding + (i + 1) * lineHeight - (lineHeight - fontSize) / 2;
|
||||||
const lineX = width / 2;
|
const lineX = width / 2;
|
||||||
@ -88,6 +108,7 @@ async function imageWithTextBlob(imageUrl, text, options = {}) {
|
|||||||
}
|
}
|
||||||
ctx.fillText(lines[i], lineX, lineY);
|
ctx.fillText(lines[i], lineX, lineY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to blob
|
// Convert to blob
|
||||||
if (canvas.convertToBlob) {
|
if (canvas.convertToBlob) {
|
||||||
return await canvas.convertToBlob({
|
return await canvas.convertToBlob({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user