You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Processes an image by overlaying a procedurally generated "New Bow" (бант).
*
* @param {HTMLImageElement} originalImg - The source image
* @param {string} bowColor - The CSS color of the bow (default: '#d60036' - crimson red)
* @param {number} bowSize - The size of the bow relative to the image width (default: 0.4)
* @param {number} positionX - Horizontal position ratio from 0 to 1 (default: 0.5)
* @param {number} positionY - Vertical position ratio from 0 to 1 (default: 0.2)
* @param {number} angle - Rotation angle in degrees (default: 0)
* @returns {HTMLCanvasElement} A canvas element containing the final merged image
*/
function processImage(originalImg, bowColor = "#d60036", bowSize = 0.4, positionX = 0.5, positionY = 0.2, angle = 0) {
// 1. Sanitize Inputs
bowSize = typeof bowSize === 'number' ? bowSize : parseFloat(bowSize);
if (isNaN(bowSize) || bowSize <= 0) bowSize = 0.4;
positionX = typeof positionX === 'number' ? positionX : parseFloat(positionX);
if (isNaN(positionX)) positionX = 0.5;
positionY = typeof positionY === 'number' ? positionY : parseFloat(positionY);
if (isNaN(positionY)) positionY = 0.2;
angle = typeof angle === 'number' ? angle : parseFloat(angle);
if (isNaN(angle)) angle = 0;
if (typeof bowColor !== 'string') bowColor = "#d60036";
// 2. Setup Canvas
const canvas = document.createElement('canvas');
canvas.width = originalImg.width;
canvas.height = originalImg.height;
const ctx = canvas.getContext('2d');
// Draw the original image as base
ctx.drawImage(originalImg, 0, 0);
// 3. Compute Bow Geometry
let width = canvas.width * bowSize;
let height = width * 0.7; // Aspect ratio of the bow parts
let cx = canvas.width * positionX;
let cy = canvas.height * positionY;
// 4. Color Processing Utilities
// Normalizes ANY valid CSS color string to base RGB to compute shading gradients
function normalizeColor(colorStr) {
const tmpCnv = document.createElement("canvas");
tmpCnv.width = 1;
tmpCnv.height = 1;
const tmpCtx = tmpCnv.getContext("2d");
tmpCtx.fillStyle = colorStr;
tmpCtx.fillRect(0, 0, 1, 1);
const data = tmpCtx.getImageData(0, 0, 1, 1).data;
return { r: data[0], g: data[1], b: data[2] };
}
function adjustColor(rgb, factor) {
let r = Math.min(255, Math.max(0, Math.floor(rgb.r * factor)));
let g = Math.min(255, Math.max(0, Math.floor(rgb.g * factor)));
let b = Math.min(255, Math.max(0, Math.floor(rgb.b * factor)));
return `rgb(${r},${g},${b})`;
}
let baseObj = normalizeColor(bowColor);
// Prevent pure black from lacking any 3D shadow depth
if (baseObj.r === 0 && baseObj.g === 0 && baseObj.b === 0) {
baseObj = { r: 25, g: 25, b: 25 };
}
const colLight = adjustColor(baseObj, 1.6);
const colBase = adjustColor(baseObj, 1.0);
const colDark = adjustColor(baseObj, 0.5);
const colStroke = adjustColor(baseObj, 0.3);
// 5. Canvas Drawing Context Configuration
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle * Math.PI / 180);
// Create 3D radial gradient for satin-like fabric
const bowGrad = ctx.createRadialGradient(0, -height * 0.2, width * 0.05, 0, 0, width * 0.6);
bowGrad.addColorStop(0, colLight);
bowGrad.addColorStop(0.3, colBase);
bowGrad.addColorStop(1, colDark);
ctx.fillStyle = bowGrad;
ctx.strokeStyle = colStroke;
ctx.lineWidth = Math.max(1, width * 0.015);
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
// Add realistic outer drop shadow
const shadowColorOverlay = "rgba(0, 0, 0, 0.4)";
ctx.shadowColor = shadowColorOverlay;
ctx.shadowBlur = Math.max(4, width * 0.04);
ctx.shadowOffsetY = Math.max(2, width * 0.02);
const w = width;
const h = height;
// 6. Draw the Components of the Bow procedurally
// Draw Ribbons First (Back layer)
function drawRibbon(isLeft) {
const s = isLeft ? -1 : 1;
ctx.beginPath();
ctx.moveTo(0, 0);
// Curve stretching downwards and outwards
ctx.bezierCurveTo(s * w * 0.05, h * 0.5, s * w * 0.15, h * 1.2, s * w * 0.25, h * 1.8);
// Inner cut for the ribbon end
ctx.lineTo(s * w * 0.1, h * 1.5);
ctx.lineTo(s * w * 0.02, h * 1.9);
// Curve back up to knot
ctx.bezierCurveTo(s * w * 0.02, h * 1.2, 0, h * 0.5, 0, 0);
ctx.fill();
// Turn off shadow for inside strokes to avoid blurry line artifacts
ctx.shadowColor = "transparent";
ctx.stroke();
// Simple fabric crease line along ribbon
ctx.beginPath();
ctx.moveTo(s * w * 0.05, h * 0.4);
ctx.lineTo(s * w * 0.08, h * 1.2);
ctx.stroke();
ctx.shadowColor = shadowColorOverlay; // restore for next piece
}
drawRibbon(true);
drawRibbon(false);
// Draw Bow Loops (Middle layer)
function drawLoop(isLeft) {
const s = isLeft ? -1 : 1;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.bezierCurveTo(s * w * 0.15, -h * 0.8, s * w * 0.7, -h * 0.6, s * w * 0.5, 0);
ctx.bezierCurveTo(s * w * 0.7, h * 0.6, s * w * 0.15, h * 0.8, 0, 0);
ctx.fill();
ctx.shadowColor = "transparent";
ctx.stroke();
// Inner folding creases to simulate pinched fabric converging on the knot
ctx.beginPath();
ctx.moveTo(s * w * 0.1, 0);
ctx.quadraticCurveTo(s * w * 0.2, 0, s * w * 0.35, h * 0.1);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(s * w * 0.1, -h * 0.05);
ctx.quadraticCurveTo(s * w * 0.2, -h * 0.1, s * w * 0.35, -h * 0.2);
ctx.stroke();
ctx.shadowColor = shadowColorOverlay;
}
drawLoop(true);
drawLoop(false);
// Draw the Central Knot (Front layer)
ctx.beginPath();
ctx.ellipse(0, 0, w * 0.1, h * 0.25, 0, 0, 2 * Math.PI);
ctx.fill();
ctx.shadowColor = "transparent";
ctx.stroke();
// Creases on the central knot
ctx.beginPath();
ctx.moveTo(-w * 0.03, -h * 0.15);
ctx.quadraticCurveTo(w * 0.03, 0, -w * 0.03, h * 0.15);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(w * 0.02, -h * 0.1);
ctx.quadraticCurveTo(-w * 0.01, 0, w * 0.02, h * 0.1);
ctx.stroke();
// Reset Context and Return
ctx.restore();
return canvas;
}
Apply Changes