You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg, numLevels = 4, contrast = 1.3, saturation = 1.2, grainAmount = 5, primaryColorName = "red") {
// Parameter validation/sanitization
numLevels = Math.max(1, Math.floor(numLevels));
contrast = Math.max(0, contrast); // contrast >= 0
saturation = Math.max(0, saturation); // saturation >= 0
grainAmount = Math.max(0, Math.floor(grainAmount));
const canvas = document.createElement('canvas');
// Using { willReadFrequently: true } can optimize if getImageData is called often,
// though for a single pass like this, its impact might be minimal.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const palettes = {
red: {
// Colors defined from darkest to lightest for intuitive mapping with luminance
steps: [
[30, 30, 30], // 0: Near Black
[139, 0, 0], // 1: Dark Red (Maroon)
[204, 0, 0], // 2: Red (Iconic Soviet Red)
[255, 80, 30], // 3: Brighter Red / Orange-Red
[255, 215, 0], // 4: Gold/Yellow (as a bright accent or highlight)
[255, 250, 205] // 5: LemonChiffon (Creamy Yellow/Highlight)
]
},
// Example of an alternative palette (not used by default)
// blue: {
// steps: [
// [20, 20, 30], // Near Black
// [0, 0, 100], // Dark Blue
// [0, 0, 205], // Medium Blue
// [70, 130, 180], // Steel Blue
// [176, 224, 230], // Powder Blue
// [220, 220, 240] // Light Lavender/Gray
// ]
// }
};
let selectedPaletteSteps = (palettes[primaryColorName] || palettes.red).steps;
// Fallback if palette definition is missing or empty
if (!selectedPaletteSteps || selectedPaletteSteps.length === 0) {
selectedPaletteSteps = [[0, 0, 0], [255, 0, 0], [255, 255, 255]];
}
const numPaletteSteps = selectedPaletteSteps.length;
// Build the activePalette based on numLevels and selectedPaletteSteps
const activePalette = [];
if (numLevels === 1) {
const midIdx = Math.min(Math.floor(numPaletteSteps / 2), numPaletteSteps - 1);
activePalette.push(selectedPaletteSteps[Math.max(0, midIdx)]); // Ensure midIdx is not negative if numPaletteSteps is 0 (covered by fallback)
} else { // numLevels >= 2
// Distribute numLevels across the available selectedPaletteSteps
for (let k = 0; k < numLevels; k++) {
// Map k from [0, numLevels-1] to an index in [0, numPaletteSteps-1]
// This ensures we use a spread of colors.
// Using Math.floor to bias towards darker/earlier colors in the steps array.
const stepIndex = Math.floor(k * (numPaletteSteps - 1) / (numLevels - 1));
activePalette.push(selectedPaletteSteps[Math.min(stepIndex, numPaletteSteps - 1)]);
}
}
// Safety: if activePalette somehow ended up empty, add at least one color
if (activePalette.length === 0 && numPaletteSteps > 0) {
activePalette.push(selectedPaletteSteps[0]);
}
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i+1];
let b = data[i+2];
// 1. Adjust contrast
// Normalize to 0-1, apply contrast, then scale back to 0-255
let r_norm = r / 255.0;
let g_norm = g / 255.0;
let b_norm = b / 255.0;
r = ((r_norm - 0.5) * contrast + 0.5) * 255.0;
g = ((g_norm - 0.5) * contrast + 0.5) * 255.0;
b = ((b_norm - 0.5) * contrast + 0.5) * 255.0;
// Clamp to 0-255
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 2. Adjust saturation
// L = 0.299R + 0.587G + 0.114B
// NewC = L + S*(C-L)
let lum = 0.299 * r + 0.587 * g + 0.114 * b;
r = Math.max(0, Math.min(255, lum + saturation * (r - lum)));
g = Math.max(0, Math.min(255, lum + saturation * (g - lum)));
b = Math.max(0, Math.min(255, lum + saturation * (b - lum)));
// 3. Calculate final luminance for posterization mapping (after contrast and saturation)
let finalLuminance = 0.299 * r + 0.587 * g + 0.114 * b;
// 4. Map to limited palette based on luminance
// Determine which color from activePalette to use
let levelIndex = 0;
if (activePalette.length > 1) {
levelIndex = Math.floor(finalLuminance * activePalette.length / 256);
levelIndex = Math.min(levelIndex, activePalette.length - 1); // Clamp to max index
} else if (activePalette.length === 1) {
levelIndex = 0; // Only one color in palette
}
// Ensure levelIndex is valid even if activePalette is empty (should be handled by safety above)
levelIndex = Math.max(0, levelIndex);
const targetColor = activePalette[levelIndex];
if (!targetColor) {
// Fallback: should not happen if activePalette is correctly populated
data[i] = r; data[i+1] = g; data[i+2] = b;
} else {
data[i] = targetColor[0];
data[i+1] = targetColor[1];
data[i+2] = targetColor[2];
}
// 5. Add grain (optional)
if (grainAmount > 0) {
// Apply grain to the final palette color for consistency
const noise = (Math.random() - 0.5) * grainAmount;
data[i] = Math.max(0, Math.min(255, data[i] + noise));
data[i+1] = Math.max(0, Math.min(255, data[i+1] + noise));
data[i+2] = Math.max(0, Math.min(255, data[i+2] + noise));
}
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Free Image Tool Creator
Can't find the image tool you're looking for? Create one based on your own needs now!
The Image Soviet Propaganda Poster Style Generator transforms images into a unique art style reminiscent of Soviet propaganda posters. Users can customize various parameters, including the number of color levels, contrast, saturation, and grain effects to create a striking visual that emulates the bold colors and design typical of this historical art form. This tool is ideal for graphic designers, artists, and individuals seeking to produce visually engaging content for social media, presentations, or artistic projects.