/* I have a PNG image which contains a colored shape (roughly in the shape of a speech bubble), surrounded by white Please write a TypeScript function (it should work in the browser) to: 1. replace all the white pixels with a transparent PNG pixel 2. replace all the colored pixels with a white pixel 3. write some input text into the colored shape 4. Make sure line returns are handled 5. It should have some padding (eg. 20px) 6. use Comic Sans MS You can use the canvas for your operation. The signature should be something like: - Please adjust the font size, based on the available number of pixels inside the bubble, taking some margin into account. - The text should not be below 8px - If there is not enough room to display it without going outside the shape, then crop the text. - in other words, NEVER write outside the shape! The function should be something like: writeIntoBubble(image: string, text: string): Promise */ export async function writeIntoBubble(image: string, text: string): Promise { const padding = 20; // Pixels return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const physicalWidth = img.width; const physicalHeight = img.height; const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { reject('Unable to get canvas context'); return; } canvas.width = physicalWidth; canvas.height = physicalHeight; ctx.drawImage(img, 0, 0, physicalWidth, physicalHeight); const imageData = ctx.getImageData(0, 0, physicalWidth, physicalHeight); const data = imageData.data; let minX = physicalWidth, minY = physicalHeight, maxX = 0, maxY = 0; for (let y = 0; y < physicalHeight; y++) { for (let x = 0; x < physicalWidth; x++) { const i = (y * physicalWidth + x) * 4; if (data[i] !== 255 || data[i + 1] !== 255 || data[i + 2] !== 255) { minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); data[i] = data[i + 1] = data[i + 2] = 255; data[i + 3] = 255; } else { data[i + 3] = 0; } } } ctx.putImageData(imageData, 0, 0); ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transforms to handle padding correctly const textX = minX + padding; const textY = minY + padding; const textWidth = (maxX - minX) - 2 * padding; const textHeight = (maxY - minY) - 2 * padding; ctx.restore(); ctx.rect(textX, textY, textWidth, textHeight); ctx.clip(); // Clip outside of the region let fontSize = 20; // Start with a large size let lines = []; do { ctx.font = `${fontSize}px Comic Sans MS`; lines = wrapText(ctx, text, textWidth); fontSize -= 2; // Reduce size and try again if text doesn't fit } while(lines.length > textHeight / fontSize && fontSize > 8); ctx.font = `${fontSize}px Comic Sans MS`; lines.forEach((line, i) => ctx.fillText(line, textX, textY + padding + i * fontSize)); resolve(canvas.toDataURL()); }; img.onerror = reject; img.src = image; }); } // Function to wrap text into lines that fit inside a specified width function wrapText(context: CanvasRenderingContext2D, text: string, maxWidth: number): string[] { const words = text.split(' '); const lines = []; let line = ''; for (let n = 0; n < words.length; n++) { const testLine = line + words[n] + ' '; const metrics = context.measureText(testLine); const testWidth = metrics.width; if (testWidth > maxWidth && n > 0) { lines.push(line); line = words[n] + ' '; } else { line = testLine; } } lines.push(line); return lines; }