You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
titleText = "ACTION HERO",
brandName = "EPIC TOYS",
ageRating = "AGES 4+",
cardBaseColor = "#0077CC", // Main color of the packaging card
cardAccentColor = "#FFD700", // Color for titles, highlights
fontFamily = "Bangers", // Font for text elements. 'Bangers' is loaded by default.
newFeatureText = "NEW!", // Text for the "NEW!" burst, e.g., "NEW!", "EXCLUSIVE!"
newFeatureTextColor = "#FFFFFF", // Color of the text in the burst
newFeatureBurstColor = "#FF0000" // Background color of the burst
) {
// Helper: Load the 'Bangers' web font (used as a default cool font)
async function loadDefaultFont() {
const bangersFontFamily = "Bangers"; // The specific font family name
const bangersFontUrl = "https://fonts.gstatic.com/s/bangers/v24/FeVQS0BTqb0h60ACL5k.woff2";
let fontAlreadyAvailable = false;
if (document.fonts) {
try {
// Check if the font is already loaded or available
if (document.fonts.check(`1em ${bangersFontFamily}`)) {
fontAlreadyAvailable = true;
}
} catch (e) {
// Some browsers (older Firefox) might throw an error on `check()`
console.warn("Font checking might not be fully supported, attempting to load font if needed.");
}
} else {
// console.log("document.fonts API not available. Cannot check/load web fonts dynamically in this environment.");
return; // Cannot proceed with font loading
}
if (!fontAlreadyAvailable) {
const font = new FontFace(bangersFontFamily, `url(${bangersFontUrl})`);
try {
await font.load();
document.fonts.add(font);
// console.log(`${bangersFontFamily} font loaded and added.`);
} catch (e) {
console.error(`Font ${bangersFontFamily} from ${bangersFontUrl} failed to load:`, e);
// If Bangers fails to load, the canvas will use the specified `fontFamily` with system fallbacks.
}
} else {
// console.log(`${bangersFontFamily} font is already available.`);
}
}
// Helper: Draw Rounded Rectangle
function roundRect(ctx, x, y, width, height, radius, fill, stroke) {
if (typeof radius === 'number') {
radius = {tl: radius, tr: radius, br: radius, bl: radius};
} else {
const defaultRadiusValues = {tl: 0, tr: 0, br: 0, bl: 0};
for (const side in defaultRadiusValues) {
radius[side] = radius[side] || defaultRadiusValues[side];
}
}
ctx.beginPath();
ctx.moveTo(x + radius.tl, y);
ctx.lineTo(x + width - radius.tr, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
ctx.lineTo(x + width, y + height - radius.br);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
ctx.lineTo(x + radius.bl, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
ctx.lineTo(x, y + radius.tl);
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
ctx.closePath();
if (fill) {
ctx.fill();
}
if (stroke) {
ctx.stroke();
}
}
// Helper: Draw Star/Burst
function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius, fillColor, strokeColor, lineWidth = 2) {
let rotation = Math.PI / 2 * 3;
let currentX = cx;
let currentY = cy;
const angleStep = Math.PI / spikes;
ctx.beginPath();
ctx.moveTo(cx, cy - outerRadius); // Start at the top point
for (let i = 0; i < spikes; i++) {
currentX = cx + Math.cos(rotation) * outerRadius;
currentY = cy + Math.sin(rotation) * outerRadius;
ctx.lineTo(currentX, currentY);
rotation += angleStep;
currentX = cx + Math.cos(rotation) * innerRadius;
currentY = cy + Math.sin(rotation) * innerRadius;
ctx.lineTo(currentX, currentY);
rotation += angleStep;
}
ctx.lineTo(cx, cy - outerRadius); // Close path back to start
ctx.closePath();
if (fillColor) {
ctx.fillStyle = fillColor;
ctx.fill();
}
if (strokeColor) {
ctx.strokeStyle = strokeColor;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
}
await loadDefaultFont(); // Ensure 'Bangers' font (or specified default) is loaded
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// --- Layout Calculations ---
const cardMinWidth = 320; // Minimum width for the packaging card
const cardMinHeight = 480; // Minimum height for the packaging card
let canvasWidth = Math.max(cardMinWidth, originalImg.width * 1.4);
// Ensure proportion for typical figure packaging if image is very narrow
if (originalImg.width * 1.8 < cardMinWidth) canvasWidth = cardMinWidth * 1.1;
let canvasHeight = canvasWidth * 1.6; // Aspect ratio similar to 6x9 inch packaging
if (canvasHeight < cardMinHeight) {
canvasHeight = cardMinHeight;
canvasWidth = Math.max(cardMinWidth, canvasHeight / 1.6);
}
// Cap maximum dimensions to prevent overly large canvases from huge images
const maxCanvasDimension = 2000;
if (canvasWidth > maxCanvasDimension || canvasHeight > maxCanvasDimension) {
const currentRatio = canvasWidth / canvasHeight;
if (canvasWidth > canvasHeight) {
canvasWidth = maxCanvasDimension;
canvasHeight = maxCanvasDimension / currentRatio;
} else {
canvasHeight = maxCanvasDimension;
canvasWidth = maxCanvasDimension * currentRatio;
}
}
canvas.width = Math.round(canvasWidth);
canvas.height = Math.round(canvasHeight);
const cardEdgePadding = canvasWidth * 0.04;
const headerAreaHeight = canvasHeight * 0.20;
const blisterDisplayAreaY = headerAreaHeight;
const blisterDisplayAreaHeight = canvasHeight * 0.65;
const footerAreaHeight = canvasHeight * 0.15;
const imgAspectRatio = originalImg.width / originalImg.height;
const maxImgDisplayWidth = canvasWidth - 2 * cardEdgePadding - (canvasWidth * 0.05); // Space for blister plastic edges
const maxImgDisplayHeight = blisterDisplayAreaHeight - 2 * cardEdgePadding - (canvasWidth * 0.05);
let scaledImgWidth, scaledImgHeight;
if ((maxImgDisplayWidth / imgAspectRatio) <= maxImgDisplayHeight) {
scaledImgWidth = maxImgDisplayWidth;
scaledImgHeight = scaledImgWidth / imgAspectRatio;
} else {
scaledImgHeight = maxImgDisplayHeight;
scaledImgWidth = scaledImgHeight * imgAspectRatio;
}
const imgDisplayX = (canvasWidth - scaledImgWidth) / 2;
const imgDisplayY = blisterDisplayAreaY + (blisterDisplayAreaHeight - scaledImgHeight) / 2;
const blisterEdgeWidth = Math.max(15, canvasWidth * 0.03); // How much blister plastic extends around image
const blisterX = imgDisplayX - blisterEdgeWidth;
const blisterY = imgDisplayY - blisterEdgeWidth;
const blisterWidth = scaledImgWidth + 2 * blisterEdgeWidth;
const blisterHeight = scaledImgHeight + 2 * blisterEdgeWidth;
const blisterCornerRadius = Math.max(10, canvasWidth * 0.05);
// --- Drawing Operations ---
// 1. Cardboard Background
ctx.fillStyle = cardBaseColor;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.strokeStyle = "rgba(0,0,0,0.15)"; // Subtle edge for the card itself
ctx.lineWidth = 1;
ctx.strokeRect(0.5, 0.5, canvasWidth - 1, canvasHeight - 1);
// 2. Peg Hook Hole (simulating a punched-out hole)
const hookHoleWidth = canvasWidth * 0.18;
const hookHoleHeight = headerAreaHeight * 0.22;
const hookHoleX = (canvasWidth - hookHoleWidth) / 2;
const hookHoleY = headerAreaHeight * 0.12; // Positioned in upper part of header
const hookHoleCornerRadius = hookHoleHeight / 3;
ctx.fillStyle = "rgba(0,0,0,0.08)"; // Darker shade to imply depth
roundRect(ctx, hookHoleX, hookHoleY, hookHoleWidth, hookHoleHeight, hookHoleCornerRadius, true, false);
ctx.strokeStyle = "rgba(0,0,0,0.15)";
ctx.lineWidth = 1;
roundRect(ctx, hookHoleX, hookHoleY, hookHoleWidth, hookHoleHeight, hookHoleCornerRadius, false, true);
// 3. Brand Name Text
let brandFontSize = canvasHeight * 0.032;
ctx.font = `italic bold ${brandFontSize}px "${fontFamily}", sans-serif`;
ctx.fillStyle = cardAccentColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Basic check to shrink font if brandName is too wide
while (ctx.measureText(brandName).width > canvasWidth * 0.7 && brandFontSize > 10) {
brandFontSize -= 1;
ctx.font = `italic bold ${brandFontSize}px "${fontFamily}", sans-serif`;
}
ctx.fillText(brandName, canvasWidth / 2, headerAreaHeight * 0.45);
// 4. Main Title Text
let titleFontSize = canvasHeight * 0.065;
ctx.font = `bold ${titleFontSize}px "${fontFamily}", sans-serif`;
const maxTitleWidth = canvasWidth * 0.90;
const titleTextMetrics = ctx.measureText(titleText);
let titleLines = [titleText];
if (titleTextMetrics.width > maxTitleWidth) {
// Attempt to split into two lines if a space exists appropriately
let potentialSplitPoint = -1;
const words = titleText.split(' ');
if (words.length > 1) {
let currentLine = "";
for(let i=0; i<words.length; i++){
const testLine = currentLine + (currentLine ? " " : "") + words[i];
if(ctx.measureText(testLine).width > maxTitleWidth && currentLine){
potentialSplitPoint = currentLine.length;
break;
}
currentLine = testLine;
}
}
if (potentialSplitPoint > 0) {
titleLines = [titleText.substring(0, potentialSplitPoint), titleText.substring(potentialSplitPoint + 1)];
} else { // If no good split, or single very long word, shrink font
while (ctx.measureText(titleText).width > maxTitleWidth && titleFontSize > 15) {
titleFontSize -=1;
ctx.font = `bold ${titleFontSize}px "${fontFamily}", sans-serif`;
}
titleLines = [titleText];
}
}
ctx.font = `bold ${titleFontSize}px "${fontFamily}", sans-serif`; // Set final font for drawing title
ctx.fillStyle = cardAccentColor;
ctx.strokeStyle = 'rgba(0,0,0,0.6)';
ctx.lineWidth = Math.max(1, titleFontSize * 0.08);
ctx.lineJoin = 'round';
ctx.textAlign = 'center';
const titleLineHeight = titleFontSize * (titleLines.length > 1 ? 0.9 : 1.1); // Tighter spacing for multi-line
const totalTitleHeight = titleLineHeight * titleLines.length;
const titleStartY = headerAreaHeight * 0.82 - (totalTitleHeight / 2) + (titleLineHeight/2) - (titleLines.length > 1 ? titleFontSize*0.1 : 0);
titleLines.forEach((line, index) => {
const lineY = titleStartY + index * titleLineHeight;
ctx.strokeText(line, canvasWidth / 2, lineY);
ctx.fillText(line, canvasWidth / 2, lineY);
});
// 5. Age Rating Text
let ageFontSize = canvasHeight * 0.028;
ctx.font = `bold ${ageFontSize}px "${fontFamily}", sans-serif`;
ctx.fillStyle = "#FFFFFF";
ctx.strokeStyle = "rgba(0,0,0,0.7)";
ctx.lineWidth = Math.max(1, ageFontSize * 0.1);
ctx.textAlign = 'right';
ctx.textBaseline = 'alphabetic'; // Common baseline for text at bottom
const ageRatingX = canvasWidth - cardEdgePadding;
const ageRatingY = canvasHeight - footerAreaHeight * 0.25;
ctx.strokeText(ageRating, ageRatingX, ageRatingY);
ctx.fillText(ageRating, ageRatingX, ageRatingY);
// 6. Draw the Original Image (as the "toy")
ctx.drawImage(originalImg, imgDisplayX, imgDisplayY, scaledImgWidth, scaledImgHeight);
// 7. Blister Plastic Bubble Effect
// Base semi-transparent fill for the bubble shape
ctx.fillStyle = 'rgba(230, 230, 255, 0.1)'; // A very light, cool, transparent fill
roundRect(ctx, blisterX, blisterY, blisterWidth, blisterHeight, blisterCornerRadius, true, false);
// "Sealed" edge effect for the blister plastic
ctx.strokeStyle = 'rgba(180, 180, 200, 0.3)'; // Color for the main sealed edge
ctx.lineWidth = Math.max(2, blisterEdgeWidth * 0.3); // Width of the seal
roundRect(ctx, blisterX, blisterY, blisterWidth, blisterHeight, blisterCornerRadius, false, true);
// Subtle highlight on the inner part of the seal
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
ctx.lineWidth = Math.max(1, blisterEdgeWidth * 0.08); // Thin highlight line
const inset = ctx.lineWidth * 2;
roundRect(ctx,
blisterX + inset, blisterY + inset,
blisterWidth - 2 * inset, blisterHeight - 2 * inset,
Math.max(5, blisterCornerRadius - inset),
false, true);
// 8. "NEW!" or Feature Burst (if text is provided)
if (newFeatureText && newFeatureText.trim() !== "") {
const numSpikes = 16;
const outerBurstRadius = canvasWidth * 0.075;
const innerBurstRadius = outerBurstRadius * 0.60;
// Position burst, e.g., top-left of blister area or card corner
const burstCenterX = blisterX + outerBurstRadius * 0.8;
const burstCenterY = blisterY + outerBurstRadius * 0.8;
drawStar(ctx, burstCenterX, burstCenterY, numSpikes, outerBurstRadius, innerBurstRadius, newFeatureBurstColor, 'rgba(0,0,0,0.25)', 2);
// Text inside the burst
let burstTextFontSize = outerBurstRadius * 0.40;
// Adjust font size roughly based on text length to prevent overflow
const textLengthFactor = Math.max(1, newFeatureText.length / 4); // Crude factor
burstTextFontSize /= textLengthFactor;
burstTextFontSize = Math.max(8, burstTextFontSize); // Minimum font size for readability
ctx.font = `bold ${burstTextFontSize}px "${fontFamily}", sans-serif`;
ctx.fillStyle = newFeatureTextColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.save(); // Save context for rotation
ctx.translate(burstCenterX, burstCenterY);
ctx.rotate(-12 * Math.PI / 180); // Slight tilt for dynamism
ctx.strokeStyle = 'rgba(0,0,0,0.5)'; // Stroke for better text visibility
ctx.lineWidth = Math.max(1, burstTextFontSize * 0.08);
ctx.strokeText(newFeatureText, 0, 0);
ctx.fillText(newFeatureText, 0, 0);
ctx.restore(); // Restore context after rotation
}
return canvas;
}
Apply Changes