You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, roundNumber = 1, overlayText = 'YOUR TURN', paletteSize = 5, hueShiftDegrees = 30) {
/**
* Dynamically loads a font from a URL if not already loaded.
* Caches the loading promise to handle concurrent calls.
* @param {string} fontFamily - The font-family name.
* @param {string} fontUrl - The URL of the font file (e.g., woff2).
*/
const loadFont = async (fontFamily, fontUrl) => {
if (!window.loadedFontPromises) {
window.loadedFontPromises = {};
}
if (!window.loadedFontPromises[fontFamily]) {
const font = new FontFace(fontFamily, `url(${fontUrl})`);
window.loadedFontPromises[fontFamily] = font.load().then(loadedFont => {
document.fonts.add(loadedFont);
}).catch(e => {
console.error(`Font '${fontFamily}' failed to load:`, e);
});
}
await window.loadedFontPromises[fontFamily];
};
/**
* Converts an RGB color value to HSL.
* @param {number} r - Red value [0, 255].
* @param {number} g - Green value [0, 255].
* @param {number} b - Blue value [0, 255].
* @returns {Array<number>} - [h, s, l] array, h in [0, 360], s,l in [0, 1].
*/
const rgbToHsl = (r, g, b) => {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h * 360, s, l];
};
/**
* Converts an HSL color value to RGB.
* @param {number} h - Hue value [0, 360].
* @param {number} s - Saturation value [0, 1].
* @param {number} l - Lightness value [0, 1].
* @returns {Array<number>} - [r, g, b] array, each in [0, 255].
*/
const hslToRgb = (h, s, l) => {
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
h /= 360;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
};
/**
* Converts RGB components to a CSS hex string.
* @param {number} r
* @param {number} g
* @param {number} b
* @returns {string} - The hex color string (e.g., "#ff0000").
*/
const rgbToHex = (r, g, b) => {
const toHex = c => ('0' + Math.round(c).toString(16)).slice(-2);
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
// 1. Load the required font
await loadFont('Bangers', 'https://fonts.gstatic.com/s/bangers/v24/FeVQS0BTqb0h60ACL5k.woff2');
// 2. Define layout constants and create the final canvas
const {
width,
height
} = originalImg;
const PALETTE_AREA_HEIGHT = 70;
const PADDING = 10;
const FONT_FAMILY = 'Bangers, cursive';
const finalCanvas = document.createElement('canvas');
finalCanvas.width = width;
finalCanvas.height = height + PALETTE_AREA_HEIGHT + PADDING;
const finalCtx = finalCanvas.getContext('2d');
finalCtx.fillStyle = '#1a1a1a';
finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
// 3. Use a temporary canvas for image processing and analysis
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
// 3a. Analyze ORIGINAL image for dominant colors
tempCtx.drawImage(originalImg, 0, 0);
const originalImageData = tempCtx.getImageData(0, 0, width, height);
const originalData = originalImageData.data;
const colorGroups = {};
const quantizationShift = 4; // Group colors by shifting bits
// For performance, sample 1 in 4 pixels
for (let i = 0; i < originalData.length; i += 16) {
const r = originalData[i], g = originalData[i + 1], b = originalData[i + 2], a = originalData[i + 3];
if (a < 128) continue; // Ignore mostly transparent pixels
const rKey = r >> quantizationShift,
gKey = g >> quantizationShift,
bKey = b >> quantizationShift;
const key = `${rKey},${gKey},${bKey}`;
if (!colorGroups[key]) {
colorGroups[key] = { r: 0, g: 0, b: 0, count: 0 };
}
colorGroups[key].r += r;
colorGroups[key].g += g;
colorGroups[key].b += b;
colorGroups[key].count++;
}
const sortedColorGroups = Object.values(colorGroups).sort((a, b) => b.count - a.count);
const dominantColors = sortedColorGroups.slice(0, paletteSize).map(group => ({
r: group.r / group.count,
g: group.g / group.count,
b: group.b / group.count,
}));
// 3b. Apply hue shift effect
for (let i = 0; i < originalData.length; i += 4) {
let [h, s, l] = rgbToHsl(originalData[i], originalData[i + 1], originalData[i + 2]);
h = (h + hueShiftDegrees) % 360;
if (h < 0) h += 360;
const [r, g, b] = hslToRgb(h, s, l);
originalData[i] = r; originalData[i + 1] = g; originalData[i + 2] = b;
}
tempCtx.putImageData(originalImageData, 0, 0);
// 4. Compose the final canvas
// 4a. Draw the processed image
finalCtx.drawImage(tempCanvas, 0, 0);
// 4b. Draw text overlay
const overlayHeight = Math.max(80, height * 0.15);
finalCtx.fillStyle = 'rgba(0, 0, 0, 0.6)';
finalCtx.fillRect(0, height - overlayHeight, width, overlayHeight);
finalCtx.fillStyle = '#FFFFFF';
finalCtx.textAlign = 'center';
finalCtx.textBaseline = 'middle';
finalCtx.font = `${overlayHeight * 0.4}px ${FONT_FAMILY}`;
finalCtx.fillText(`ROUND ${roundNumber}`, width / 2, height - overlayHeight / 2 - (overlayHeight * 0.1));
finalCtx.font = `${overlayHeight * 0.25}px ${FONT_FAMILY}`;
finalCtx.fillText(overlayText, width / 2, height - overlayHeight / 2 + (overlayHeight * 0.25));
// 4c. Draw color palette
const paletteY = height + PADDING;
const swatchWidth = (width - (paletteSize + 1) * PADDING) / Math.max(1, paletteSize);
const swatchHeight = PALETTE_AREA_HEIGHT - 25;
finalCtx.textAlign = 'center';
for (let i = 0; i < dominantColors.length; i++) {
const color = dominantColors[i];
const hex = rgbToHex(color.r, color.g, color.b);
const x = PADDING + i * (swatchWidth + PADDING);
finalCtx.fillStyle = hex;
finalCtx.fillRect(x, paletteY, swatchWidth, swatchHeight);
if (swatchWidth > 40) {
const luminance = (0.299 * color.r + 0.587 * color.g + 0.114 * color.b) / 255;
finalCtx.fillStyle = luminance > 0.5 ? '#000000' : '#FFFFFF';
finalCtx.font = '12px monospace';
finalCtx.textBaseline = 'bottom';
finalCtx.fillText(hex, x + swatchWidth / 2, paletteY + swatchHeight - 4);
}
}
finalCtx.fillStyle = '#CCCCCC';
finalCtx.textBaseline = 'top';
finalCtx.font = '12px sans-serif';
finalCtx.fillText('DOMINANT COLORS', width / 2, paletteY + swatchHeight + 5);
// 5. Return canvas
return finalCanvas;
}
Apply Changes