You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, topText = "JOIN THE COLONY!", bottomText = "YOUR FUTURE AWAITS!", colonyName = "MARS PRIME", primaryColor = "#D92323", secondaryColor = "#0A2F51", textColor = "#F0E6D2", imageFilter = "duotone", overlayPattern = "stars", fontFamily = "Impact, Arial, sans-serif") {
// Helper to parse hex color string to RGB object
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 0, g: 0, b: 0 }; // Default to black if parse fails
}
// Helper to draw stroked text
function drawStrokedText(ctx, text, x, y, fontSize, font, fillColor, strokeColor, strokeWidth) {
ctx.font = `${fontSize}px ${font}`;
ctx.fillStyle = fillColor;
ctx.strokeStyle = strokeColor;
ctx.lineWidth = strokeWidth;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
if (strokeWidth > 0 && strokeColor && strokeColor !== 'transparent') {
ctx.strokeText(text, x, y);
}
ctx.fillText(text, x, y);
}
// Helper functions for patterns (designed to be subtle)
function drawStars(ctx, width, height, count, color) {
const originalFillStyle = ctx.fillStyle;
const originalAlpha = ctx.globalAlpha;
ctx.fillStyle = color;
ctx.globalAlpha *= 0.6; // Make stars somewhat transparent
for (let i = 0; i < count; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const r = Math.random() * 1.5 + 0.5; // Star radius
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = originalFillStyle;
ctx.globalAlpha = originalAlpha;
}
function drawGrid(ctx, width, height, spacing, color) {
const originalStrokeStyle = ctx.strokeStyle;
const originalLineWidth = ctx.lineWidth;
const originalAlpha = ctx.globalAlpha;
ctx.strokeStyle = color;
ctx.lineWidth = 0.5; // Thin grid lines
ctx.globalAlpha *= 0.15; // Very subtle grid
for (let x = 0.5; x < width; x += spacing) { // Offset by 0.5 for crisp lines
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
for (let y = 0.5; y < height; y += spacing) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
ctx.strokeStyle = originalStrokeStyle;
ctx.lineWidth = originalLineWidth;
ctx.globalAlpha = originalAlpha;
}
function drawScanlines(ctx, width, height, lineThickness, color) {
const originalFillStyle = ctx.fillStyle;
const originalAlpha = ctx.globalAlpha;
ctx.fillStyle = color;
ctx.globalAlpha *= 0.1; // Subtle scanlines
for (let y = 0; y < height; y += lineThickness * 2) {
ctx.fillRect(0, y, width, lineThickness);
}
ctx.fillStyle = originalFillStyle;
ctx.globalAlpha = originalAlpha;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Standard poster dimensions (e.g., 2:3 ratio like 800x1200)
const posterWidth = 800;
const posterHeight = 1200;
canvas.width = posterWidth;
canvas.height = posterHeight;
// 1. Background Fill (covers the whole poster canvas)
// A dark, slightly desaturated secondary color can work well as a base
const baseBgColor = hexToRgb(secondaryColor);
ctx.fillStyle = `rgb(${baseBgColor.r*0.5}, ${baseBgColor.g*0.5}, ${baseBgColor.b*0.5})`; // Darker version of secondary
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 2. Image Processing and Drawing
const tempImageCanvas = document.createElement('canvas');
tempImageCanvas.width = originalImg.width;
tempImageCanvas.height = originalImg.height;
const tempImageCtx = tempImageCanvas.getContext('2d');
tempImageCtx.drawImage(originalImg, 0, 0);
if (imageFilter !== "none") {
const imageData = tempImageCtx.getImageData(0, 0, tempImageCanvas.width, tempImageCanvas.height);
const data = imageData.data;
if (imageFilter === "grayscale") {
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; data[i + 1] = avg; data[i + 2] = avg;
}
tempImageCtx.putImageData(imageData, 0, 0);
} else if (imageFilter === "duotone") {
const rgbPrimaryHighlight = hexToRgb(primaryColor); // Brighter/main color for highlights
const rgbSecondaryShadow = hexToRgb(secondaryColor); // Darker/accent color for shadows
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i + 1], b = data[i + 2];
const luminance = 0.299 * r + 0.587 * g + 0.114 * b; // perceptual luminance
const t = luminance / 255; // Factor: 0 for black, 1 for white
data[i] = Math.round(rgbSecondaryShadow.r * (1 - t) + rgbPrimaryHighlight.r * t);
data[i+1] = Math.round(rgbSecondaryShadow.g * (1 - t) + rgbPrimaryHighlight.g * t);
data[i+2] = Math.round(rgbSecondaryShadow.b * (1 - t) + rgbPrimaryHighlight.b * t);
}
tempImageCtx.putImageData(imageData, 0, 0);
} else if (imageFilter === "colorize") {
// Grayscale first
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; data[i + 1] = avg; data[i + 2] = avg;
}
tempImageCtx.putImageData(imageData, 0, 0); // Put grayscale data back
// Then apply color blend
tempImageCtx.globalCompositeOperation = 'color'; // 'color' blend mode for tinting
tempImageCtx.fillStyle = primaryColor;
tempImageCtx.globalAlpha = 0.75; // Apply tint with some transparency
tempImageCtx.fillRect(0, 0, tempImageCanvas.width, tempImageCanvas.height);
tempImageCtx.globalAlpha = 1.0; // Reset alpha
tempImageCtx.globalCompositeOperation = 'source-over'; // Reset blend mode
}
}
// Calculate image destination size to fit originalImg (from tempImageCanvas)
// into the posterWidth x posterHeight main_canvas, preserving aspect ratio
let imgDrawX = 0, imgDrawY = 0, imgDrawWidth = canvas.width, imgDrawHeight = canvas.height;
const imgAspect = tempImageCanvas.width / tempImageCanvas.height;
const canvasAspect = canvas.width / canvas.height;
if (imgAspect > canvasAspect) { // Image is wider than canvas (letterbox)
imgDrawHeight = canvas.width / imgAspect;
imgDrawY = (canvas.height - imgDrawHeight) / 2;
} else { // Image is taller or same aspect (pillarbox)
imgDrawWidth = canvas.height * imgAspect;
imgDrawX = (canvas.width - imgDrawWidth) / 2;
}
ctx.drawImage(tempImageCanvas, imgDrawX, imgDrawY, imgDrawWidth, imgDrawHeight);
// 3. Overlay Pattern (drawn over the image and background areas)
if (overlayPattern === "stars") {
drawStars(ctx, canvas.width, canvas.height, 200, textColor); // More stars
} else if (overlayPattern === "grid") {
drawGrid(ctx, canvas.width, canvas.height, 40, 'rgba(200,200,200,1)'); // Adjusted grid
} else if (overlayPattern === "scanlines") {
drawScanlines(ctx, canvas.width, canvas.height, 3, 'rgba(255,255,255,1)'); // Lighter scanlines for visibility
}
// 4. Decorative Elements (e.g., planet symbol)
ctx.fillStyle = primaryColor;
ctx.globalAlpha = 0.75;
const planetRadius = Math.min(canvas.width, canvas.height) / 12;
const planetX = canvas.width * 0.82; // Top-rightish
const planetY = canvas.height * 0.15;
ctx.beginPath();
ctx.arc(planetX, planetY, planetRadius, 0, Math.PI * 2);
ctx.fill();
// Rings for the planet
ctx.strokeStyle = textColor;
ctx.lineWidth = Math.max(1.5, planetRadius / 12);
ctx.beginPath(); // Inner ring
ctx.ellipse(planetX, planetY, planetRadius * 1.3, planetRadius * 0.4, Math.PI / -7, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath(); // Outer ring
ctx.ellipse(planetX, planetY, planetRadius * 1.6, planetRadius * 0.55, Math.PI / -7, 0, Math.PI * 2);
ctx.stroke();
ctx.globalAlpha = 1.0;
// 5. Text Rendering
const textStrokeColor = 'rgba(0,0,0,0.7)'; // Slightly transparent black for softer stroke
const textStrokeWidth = 2;
// Top Text Block
const topTextSize = Math.max(32, canvas.height / 25);
const topTextYPos = canvas.height * 0.08;
ctx.fillStyle = primaryColor;
// Angled block for top text
const topBlockHeight = topTextSize * 1.5;
ctx.beginPath();
ctx.moveTo(0, topTextYPos - topBlockHeight / 2 - topBlockHeight*0.1);
ctx.lineTo(canvas.width, topTextYPos - topBlockHeight / 2 + topBlockHeight*0.1);
ctx.lineTo(canvas.width, topTextYPos + topBlockHeight / 2 + topBlockHeight*0.1);
ctx.lineTo(0, topTextYPos + topBlockHeight / 2 - topBlockHeight*0.1);
ctx.closePath();
ctx.fill();
drawStrokedText(ctx, topText.toUpperCase(), canvas.width / 2, topTextYPos, topTextSize, fontFamily, textColor, textStrokeColor, textStrokeWidth);
// Bottom Text Block
const bottomTextSize = Math.max(30, canvas.height / 28);
const bottomTextYPos = canvas.height * 0.92;
ctx.fillStyle = primaryColor;
// Angled block for bottom text
const bottomBlockHeight = bottomTextSize * 1.5;
ctx.beginPath();
ctx.moveTo(0, bottomTextYPos - bottomBlockHeight / 2 + bottomBlockHeight*0.1);
ctx.lineTo(canvas.width, bottomTextYPos - bottomBlockHeight / 2 - bottomBlockHeight*0.1);
ctx.lineTo(canvas.width, bottomTextYPos + bottomBlockHeight / 2 - bottomBlockHeight*0.1);
ctx.lineTo(0, bottomTextYPos + bottomBlockHeight / 2 + bottomBlockHeight*0.1);
ctx.closePath();
ctx.fill();
drawStrokedText(ctx, bottomText.toUpperCase(), canvas.width / 2, bottomTextYPos, bottomTextSize, fontFamily, textColor, textStrokeColor, textStrokeWidth);
// Colony Name / Main Slogan
const colonyNameSize = Math.max(60, canvas.height / 15);
const bannerHeight = colonyNameSize * 1.5; // Adjusted for better proportion
const bannerY = canvas.height * 0.75; // Centered lower on the poster
ctx.save(); // Save context for banner drawing
ctx.beginPath();
// Make banner extend slightly off-canvas for a bolder look if desired
const bannerBleed = 30;
ctx.moveTo(-bannerBleed, bannerY - bannerHeight * 0.1); // Top-left point, slight upward angle
ctx.lineTo(canvas.width + bannerBleed, bannerY + bannerHeight * 0.1); // Top-right point
ctx.lineTo(canvas.width + bannerBleed, bannerY + bannerHeight - bannerHeight * 0.1); // Bottom-right
ctx.lineTo(-bannerBleed, bannerY + bannerHeight + bannerHeight * 0.1); // Bottom-left
ctx.closePath();
ctx.fillStyle = secondaryColor;
ctx.fill();
ctx.strokeStyle = textColor; // Outline for the banner itself
ctx.lineWidth = Math.max(2, colonyNameSize / 25);
ctx.stroke();
ctx.restore(); // Restore context
// Calculate center Y of the slightly skewed banner for text placement
const textBannerCenterY = bannerY + bannerHeight / 2;
drawStrokedText(ctx, colonyName.toUpperCase(), canvas.width / 2, textBannerCenterY, colonyNameSize, fontFamily, textColor, textStrokeColor, textStrokeWidth + 1);
// 6. Final Border
const borderWidth = Math.max(10, canvas.width / 70); // Slightly thicker border
ctx.strokeStyle = primaryColor;
ctx.lineWidth = borderWidth;
ctx.strokeRect(borderWidth / 2, borderWidth / 2, canvas.width - borderWidth, canvas.height - borderWidth);
// Optional inner accent border with the text color
const accentBorderWidth = Math.max(2, borderWidth / 4);
ctx.strokeStyle = textColor;
ctx.lineWidth = accentBorderWidth;
const accentOffset = borderWidth + accentBorderWidth / 2 + 3; // Gap from main border
ctx.strokeRect(accentOffset, accentOffset, canvas.width - accentOffset * 2, canvas.height - accentOffset * 2);
return canvas;
}
Apply Changes