You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
style = 'long',
color = '#2E1A11',
volume = 100,
faceCenterX = 50,
faceCenterY = 40,
faceWidth = 22
) {
// Determine canvas dimensions
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
// Create the main canvas and draw the original image
const mainCanvas = document.createElement('canvas');
mainCanvas.width = width;
mainCanvas.height = height;
const ctx = mainCanvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0, width, height);
// Calculate face bounding box based on percentage parameters
const cx = width * (faceCenterX / 100);
const cy = height * (faceCenterY / 100);
const fw = width * (faceWidth / 100);
const fh = fw * 1.35; // Standard human face aspect ratio
const rX = fw / 2;
const rY = fh / 2;
const vol = volume / 100;
// Create a temporary canvas to draw and mask the hair
const hairCanvas = document.createElement('canvas');
hairCanvas.width = width;
hairCanvas.height = height;
const hCtx = hairCanvas.getContext('2d');
// Configure brush settings for hair
hCtx.fillStyle = color;
hCtx.strokeStyle = color;
hCtx.lineJoin = 'round';
hCtx.lineCap = 'round';
style = style.toLowerCase();
const validStyles = ['bob', 'long', 'spiky', 'curly'];
if (!validStyles.includes(style)) style = 'long';
if (style === 'bob') {
// Main mass of bob cut
hCtx.beginPath();
hCtx.ellipse(cx, cy - rY * 0.1, rX * 1.6 * vol, rY * 1.5 * vol, 0, 0, Math.PI * 2);
hCtx.fill();
// Subtractive cutout for the face
hCtx.globalCompositeOperation = 'destination-out';
hCtx.beginPath();
hCtx.ellipse(cx, cy + rY * 0.1, rX * 0.95, rY * 1.05, 0, 0, Math.PI * 2);
hCtx.fill();
hCtx.globalCompositeOperation = 'source-over';
// Bangs over the forehead
hCtx.beginPath();
hCtx.moveTo(cx - rX * 1.2 * vol, cy - rY * 0.4);
hCtx.quadraticCurveTo(cx, cy - rY * 0.1, cx + rX * 1.2 * vol, cy - rY * 0.4);
hCtx.lineTo(cx + rX * 1.5 * vol, cy - rY * 1.8 * vol);
hCtx.lineTo(cx - rX * 1.5 * vol, cy - rY * 1.8 * vol);
hCtx.fill();
} else if (style === 'long') {
// Main mass flowing down
hCtx.beginPath();
hCtx.ellipse(cx, cy + rY * 0.5, rX * 1.7 * vol, rY * 2.5 * vol, 0, 0, Math.PI * 2);
hCtx.fill();
// Subtractive cutout for the face
hCtx.globalCompositeOperation = 'destination-out';
hCtx.beginPath();
hCtx.ellipse(cx, cy + rY * 0.1, rX * 0.95, rY * 1.05, 0, 0, Math.PI * 2);
hCtx.fill();
hCtx.globalCompositeOperation = 'source-over';
// Parted hair sweeps from top center to sides
hCtx.beginPath();
hCtx.moveTo(cx, cy - rY * 1.8 * vol);
hCtx.quadraticCurveTo(cx - rX * 0.2, cy - rY * 0.5, cx - rX * 1.1, cy - rY * 0.1);
hCtx.lineTo(cx - rX * 1.8 * vol, cy - rY * 1.8 * vol);
hCtx.fill();
hCtx.beginPath();
hCtx.moveTo(cx, cy - rY * 1.8 * vol);
hCtx.quadraticCurveTo(cx + rX * 0.2, cy - rY * 0.5, cx + rX * 1.1, cy - rY * 0.1);
hCtx.lineTo(cx + rX * 1.8 * vol, cy - rY * 1.8 * vol);
hCtx.fill();
} else if (style === 'spiky') {
// No full face cutout required; rest entirely on upper half of head
hCtx.beginPath();
hCtx.moveTo(cx - rX * 1.05, cy - rY * 0.6);
const spikes = 15;
for(let i = 0; i <= spikes; i++) {
let pct = i / spikes;
let x = cx - rX * 1.05 + pct * (rX * 2.1);
let arch = Math.sin(pct * Math.PI);
let yBase = cy - rY * 0.6 - arch * rY * 0.3 * vol;
let yTip = yBase - rY * 0.6 * vol - (Math.random() * rY * 0.4);
if (i === 0) {
hCtx.lineTo(x, yBase);
} else {
hCtx.lineTo(x - (rX * 2.1) / (spikes * 2), yTip);
hCtx.lineTo(x, yBase);
}
}
// Sweep underneath the spikes to cover hairline
hCtx.quadraticCurveTo(cx, cy - rY * 0.9, cx - rX * 1.05, cy - rY * 0.6);
hCtx.fill();
} else if (style === 'curly') {
// Voluminous base
hCtx.beginPath();
hCtx.ellipse(cx, cy + rY * 0.2, rX * 1.8 * vol, rY * 2.0 * vol, 0, 0, Math.PI * 2);
hCtx.fill();
// Loop generating outer curly texture clouds
for (let a = 0; a < Math.PI * 2; a += 0.2) {
let radX = rX * 1.6 * vol + Math.random() * rX * 0.3;
let radY = rY * 1.8 * vol + Math.random() * rY * 0.3;
let oX = cx + Math.cos(a) * radX;
let oY = cy + Math.sin(a) * radY;
hCtx.beginPath();
hCtx.arc(oX, oY, rX * 0.5 * vol, 0, Math.PI * 2);
hCtx.fill();
}
// Subtractive cutout for the face
hCtx.globalCompositeOperation = 'destination-out';
hCtx.beginPath();
hCtx.ellipse(cx, cy + rY * 0.1, rX * 0.95, rY * 1.05, 0, 0, Math.PI * 2);
hCtx.fill();
hCtx.globalCompositeOperation = 'source-over';
// Add overlapping curly bangs across the front top
for (let x = cx - rX * 0.9; x <= cx + rX * 0.9; x += rX * 0.35) {
hCtx.beginPath();
hCtx.arc(x, cy - rY * 0.65, rX * 0.35 * vol, 0, Math.PI * 2);
hCtx.fill();
}
}
// Add texture highlights (strands) bounded directly to the drawn hair
hCtx.globalCompositeOperation = 'source-atop';
hCtx.lineWidth = Math.max(1, width * 0.002);
for (let i = 0; i < 250; i++) {
// Randomly choose subtle dark shadows or light highlights
hCtx.strokeStyle = Math.random() > 0.5 ? 'rgba(255, 255, 255, 0.06)' : 'rgba(0, 0, 0, 0.15)';
hCtx.beginPath();
let sx = cx + (Math.random() * 6 - 3) * rX * vol;
let sy = cy - rY * 2 * vol + (Math.random() * 2) * rY;
hCtx.moveTo(sx, sy);
let cpX = sx + (Math.random() * rX - rX / 2);
let cpY = sy + rY * 1.5;
let ex = sx + (Math.random() * rX * 2 - rX);
let ey = sy + rY * 4 * vol;
if (style === 'curly') {
hCtx.arc(sx, sy, Math.random() * rX * 0.4, 0, Math.PI * 2);
} else if (style === 'spiky') {
ex = sx + (Math.random() * rX - rX / 2);
ey = sy - rY * 1.5 * vol;
hCtx.quadraticCurveTo(cpX, cpY, ex, ey);
} else {
hCtx.quadraticCurveTo(cpX, cpY, ex, ey);
}
hCtx.stroke();
}
// Reset compositing and blend hair to the main canvas
hCtx.globalCompositeOperation = 'source-over';
// Draw the temporary hair canvas onto the final canvas
ctx.drawImage(hairCanvas, 0, 0);
return mainCanvas;
}
Apply Changes