You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Fairytale Tourist Image Generator
* Generates a magical, stylized "fairytale" portrait from the given image.
* Converts the image into a soft-edged magical mirror reflection enveloped in fantasy elements.
*
* @param {HTMLImageElement} originalImg - The source photo.
* @param {string} captionText - The text displayed at the bottom of the image.
* @param {number|string} magicHue - Base hue (0-360) for the magical atmosphere (default 280 = Purple).
* @param {number|string} glowIntensity - Scale of the sparkle and magical bloom (default 1.0).
* @returns {HTMLCanvasElement} - The created fairytale canvas.
*/
async function processImage(originalImg, captionText = "Турист из сказки", magicHue = 280, glowIntensity = 1.0) {
const hue = parseFloat(magicHue) || 280;
const intensity = parseFloat(glowIntensity) || 1.0;
// 1. Dynamically load a beautiful "fairytale" font that supports Cyrillic & Latin
const fontName = 'Marck Script';
try {
if (!document.getElementById('fairytale-font-style')) {
const style = document.createElement('style');
style.id = 'fairytale-font-style';
style.innerHTML = `@import url('https://fonts.googleapis.com/css2?family=Marck+Script&display=swap');`;
document.head.appendChild(style);
}
// Give the font a moment to stream and parse if not cached
await new Promise(resolve => setTimeout(resolve, 800));
} catch (e) {
console.warn('Could not load Google Font. Falling back to default cursive.', e);
}
// 2. Setup Dimensions
const maxImgSize = 600;
let scale = 1;
if (originalImg.width > maxImgSize || originalImg.height > maxImgSize) {
scale = Math.min(maxImgSize / originalImg.width, maxImgSize / originalImg.height);
}
const imgW = originalImg.width * scale;
const imgH = originalImg.height * scale;
const horizontalPadding = 100;
const topPadding = 100;
const bottomPadding = 180; // Extra space for text
const canvas = document.createElement('canvas');
canvas.width = imgW + (horizontalPadding * 2);
canvas.height = imgH + topPadding + bottomPadding;
const ctx = canvas.getContext('2d');
// 3. Draw Magical Night/Fantasy Background
const cx = canvas.width / 2;
const cy = topPadding + (imgH / 2);
const bgGradient = ctx.createRadialGradient(cx, cy, imgW * 0.2, cx, cy, canvas.width * 0.8);
bgGradient.addColorStop(0, `hsl(${hue}, 40%, 20%)`);
bgGradient.addColorStop(0.6, `hsl(${hue - 20}, 60%, 8%)`);
bgGradient.addColorStop(1, `#050510`);
ctx.fillStyle = bgGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 4. Create Soft Edge Mask for the Portrait
const offCanvas = document.createElement('canvas');
offCanvas.width = imgW;
offCanvas.height = imgH;
const octx = offCanvas.getContext('2d');
// Draw an elliptical fuzzy mask
const ellipseRadiusX = (imgW / 2) - 40;
const ellipseRadiusY = (imgH / 2) - 40;
octx.shadowColor = 'black';
octx.shadowBlur = 40;
octx.fillStyle = 'black';
octx.beginPath();
octx.ellipse(imgW / 2, imgH / 2, Math.max(1, ellipseRadiusX), Math.max(1, ellipseRadiusY), 0, 0, Math.PI * 2);
octx.fill();
// Map original image onto the alpha of the fuzzy ellipse
octx.globalCompositeOperation = 'source-in';
octx.shadowBlur = 0; // Turn off shadow so it doesn't double-apply to image
octx.drawImage(originalImg, 0, 0, imgW, imgH);
// 5. Apply the Portrait to Main Canvas
// Base portrait
ctx.drawImage(offCanvas, horizontalPadding, topPadding);
// Magical bloom / glow overlay
ctx.save();
ctx.globalCompositeOperation = 'screen';
ctx.filter = `blur(${10 * intensity}px)`;
ctx.globalAlpha = 0.5 * intensity;
ctx.drawImage(offCanvas, horizontalPadding, topPadding);
ctx.restore();
// 6. Draw Magical Frame/Aura around Portrait
ctx.save();
ctx.beginPath();
ctx.ellipse(cx, cy, Math.max(1, ellipseRadiusX + 5), Math.max(1, ellipseRadiusY + 5), 0, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255, 230, 150, 0.4)`;
ctx.lineWidth = 2;
ctx.shadowColor = `hsl(${hue + 40}, 100%, 70%)`;
ctx.shadowBlur = 15 * intensity;
ctx.stroke();
ctx.beginPath();
ctx.ellipse(cx, cy, Math.max(1, ellipseRadiusX + 25), Math.max(1, ellipseRadiusY + 25), 0, 0, Math.PI * 2);
ctx.strokeStyle = `rgba(255, 255, 255, 0.1)`;
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
// 7. Add Procedural Fairytale Elements (Fairy Dust & Sparkles)
const drawSparkle = (x, y, size) => {
ctx.save();
ctx.translate(x, y);
ctx.beginPath();
ctx.moveTo(0, -size);
ctx.quadraticCurveTo(0, 0, size, 0);
ctx.quadraticCurveTo(0, 0, 0, size);
ctx.quadraticCurveTo(0, 0, -size, 0);
ctx.quadraticCurveTo(0, 0, 0, -size);
ctx.fillStyle = '#FFF2D8';
ctx.shadowColor = '#FFFFFF';
ctx.shadowBlur = 10 * intensity;
ctx.fill();
ctx.restore();
};
// Fairy dust dots
const numDust = 80 * intensity;
for (let i = 0; i < numDust; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const radius = Math.random() * 2 + 0.5;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = Math.random() > 0.5 ? '#ffffff' : `hsl(${hue + 30}, 100%, 80%)`;
ctx.globalAlpha = Math.random() * 0.8 + 0.2;
ctx.shadowBlur = 5;
ctx.shadowColor = ctx.fillStyle;
ctx.fill();
}
ctx.globalAlpha = 1.0;
ctx.shadowBlur = 0;
// Glowing Star Sparkles
const numSparkles = 20 * intensity;
for (let i = 0; i < numSparkles; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const size = Math.random() * 12 + 4;
// Avoid drawing huge sparkles right over the main face area
const distToCenter = Math.hypot(x - cx, y - cy);
if (distToCenter > (imgW / 4)) {
drawSparkle(x, y, size);
}
}
// 8. Draw Fairytale Typography (Caption)
const textY = canvas.height - (bottomPadding * 0.5);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = `56px "${fontName}", cursive`;
// Text Glow
ctx.shadowColor = `hsl(${hue}, 80%, 30%)`;
ctx.shadowBlur = 20;
// Gradient Gold Text Fill
const textGradient = ctx.createLinearGradient(0, textY - 30, 0, textY + 30);
textGradient.addColorStop(0, '#FFE891');
textGradient.addColorStop(0.5, '#FBBF24');
textGradient.addColorStop(1, '#B45309');
ctx.fillStyle = textGradient;
ctx.fillText(captionText, cx, textY);
// Slight bright overlay to make text pop
ctx.shadowBlur = 0;
ctx.globalCompositeOperation = 'screen';
ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
ctx.fillText(captionText, cx, textY - 2);
ctx.globalCompositeOperation = 'source-over';
return canvas;
}
Apply Changes