You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg,
scrollColor = '#f5f5dc', // Beige for paper
rollerColor = '#8B4513', // SaddleBrown for wood
topRollerHeight = 50,
bottomRollerHeight = 70,
rollerEndWidth = 30, // Width of roller ends sticking out beyond paper
imagePadding = 20, // Padding on paper around the image itself
titleText = '', // Vertical title text, e.g., "山水清音"
titleFont = "30px 'KaiTi', 'SimSun', 'STKaiti', 'PMingLiU', 'Heiti TC', 'LiSu', serif", // Font for title
titleColor = '#000000', // Color for title
sealText = '', // Text for seal, e.g., "印" or "山人". Max 4 chars for default layout.
sealColor = '#E80000', // Red for seal background
sealSize = 40 // Size of the seal square
) {
// Ensure parameters are of correct type after getting defaults
const p_scrollColor = String(scrollColor);
const p_rollerColor = String(rollerColor);
const p_topRollerHeight = Number(topRollerHeight);
const p_bottomRollerHeight = Number(bottomRollerHeight);
const p_rollerEndWidth = Number(rollerEndWidth);
const p_imagePadding = Number(imagePadding);
const p_titleText = String(titleText);
const p_titleFont = String(titleFont);
const p_titleColor = String(titleColor);
const p_titleVerticalGap = 5; // Gap between characters in vertical title
const p_sealText = String(sealText);
const p_sealColor = String(sealColor);
const p_sealSize = Number(sealSize);
// Dynamically determine seal font size based on sealSize and number of characters
let derivedSealFontSizeFactor = 0.55; // Default for 1-2 chars
if (p_sealText.length > 2 && p_sealText.length <= 4) derivedSealFontSizeFactor = 0.35; // For 3-4 chars in a 2x2 grid
else if (p_sealText.length > 4) derivedSealFontSizeFactor = 0.25; // For more chars, reduce further (though layout limited to 4 now)
const derivedSealFontSize = Math.max(8, Math.floor(p_sealSize * derivedSealFontSizeFactor)); // Min 8px font
const p_sealFont = `bold ${derivedSealFontSize}px 'KaiTi', 'SimSun', 'STKaiti', 'PMingLiU', 'Heiti TC', 'LiSu', serif`;
// Helper function to lighten a hex color
function _lightenColor(hex, percent) {
hex = hex.replace(/^#/, '');
const num = parseInt(hex, 16);
const amt = Math.round(2.55 * percent);
const R = Math.min(255, (num >> 16) + amt);
const G = Math.min(255, (num >> 8 & 0x00FF) + amt);
const B = Math.min(255, (num & 0x0000FF) + amt);
return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
}
// Helper function to darken a hex color
function _darkenColor(hex, percent) {
hex = hex.replace(/^#/, '');
const num = parseInt(hex, 16);
const amt = Math.round(2.55 * percent);
const R = Math.max(0, (num >> 16) - amt);
const G = Math.max(0, (num >> 8 & 0x00FF) - amt);
const B = Math.max(0, (num & 0x0000FF) - amt);
return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imageDisplayWidth = originalImg.width;
const imageDisplayHeight = originalImg.height;
// Calculate title dimensions
let titleActualRenderWidth = 0;
let titleActualRenderHeight = 0;
let titleFontSize = 30; // Default, will be parsed from p_titleFont
if (p_titleText) {
ctx.font = p_titleFont;
const fontMatch = p_titleFont.match(/(\d+)px/);
if (fontMatch && fontMatch[1]) {
titleFontSize = parseInt(fontMatch[1], 10);
}
const titleChars = p_titleText.split('');
if (titleChars.length > 0) {
let maxCharWidth = 0;
titleChars.forEach(char => {
const metrics = ctx.measureText(char);
if (metrics.width > maxCharWidth) maxCharWidth = metrics.width;
});
titleActualRenderWidth = maxCharWidth;
titleActualRenderHeight = titleChars.length * titleFontSize + Math.max(0, titleChars.length - 1) * p_titleVerticalGap;
}
}
// Width needed for the title column, including some horizontal padding.
const titleAreaRequiredWidth = p_titleText ? titleActualRenderWidth + 20 : 0;
// Calculate dimensions for the "paper" part of the scroll
// This is the area where image and title are drawn.
const paperContentWidth = imageDisplayWidth + 2 * p_imagePadding + titleAreaRequiredWidth;
const paperContentHeight = imageDisplayHeight + 2 * p_imagePadding;
// Paper height must also accommodate the title if it's very long.
const minPaperHeightForTitle = titleActualRenderHeight + 2 * p_imagePadding;
const finalPaperHeight = Math.max(paperContentHeight, minPaperHeightForTitle);
// Canvas final dimensions
canvas.width = paperContentWidth + 2 * p_rollerEndWidth;
canvas.height = finalPaperHeight + p_topRollerHeight + p_bottomRollerHeight;
// Coordinates for the paper rectangle
const paperX = p_rollerEndWidth;
const paperY = p_topRollerHeight;
const paperDrawableWidth = paperContentWidth; // The width of the "paper" excluding roller ends
const paperDrawableHeight = finalPaperHeight; // The height of the "paper"
// 1. Draw scroll paper background
ctx.fillStyle = p_scrollColor;
ctx.fillRect(paperX, paperY, paperDrawableWidth, paperDrawableHeight);
// Optional: A thin border for the paper area
ctx.strokeStyle = _darkenColor(p_scrollColor, 15);
ctx.lineWidth = 1;
ctx.strokeRect(paperX, paperY, paperDrawableWidth, paperDrawableHeight);
// Helper function to draw roller ends (caps)
function _drawRollerEndCap(x, y, radius, baseColor) {
ctx.fillStyle = _darkenColor(baseColor, 30); // Darkest part for depth
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = baseColor; // Main color
ctx.beginPath();
ctx.arc(x, y, radius * 0.9, 0, 2 * Math.PI);
ctx.fill();
// Highlight on the cap
const highlightGradient = ctx.createRadialGradient(
x - radius * 0.3, y - radius * 0.3, radius * 0.1,
x, y, radius * 0.8
);
highlightGradient.addColorStop(0, _lightenColor(baseColor, 40));
highlightGradient.addColorStop(0.5, _lightenColor(baseColor, 20));
highlightGradient.addColorStop(1, baseColor);
ctx.fillStyle = highlightGradient;
ctx.beginPath();
ctx.arc(x, y, radius * 0.85, 0, 2 * Math.PI);
ctx.fill();
}
// 2. Draw Top Roller
ctx.fillStyle = p_rollerColor;
ctx.fillRect(0, 0, canvas.width, p_topRollerHeight);
// Shading for 3D effect on roller body
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillRect(0, p_topRollerHeight - Math.min(5, p_topRollerHeight * 0.1), canvas.width, Math.min(5, p_topRollerHeight * 0.1));
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(0, 0, canvas.width, Math.min(5, p_topRollerHeight * 0.1));
// Draw top roller end caps
_drawRollerEndCap(p_rollerEndWidth / 2, p_topRollerHeight / 2, p_topRollerHeight * 0.4, p_rollerColor);
_drawRollerEndCap(canvas.width - p_rollerEndWidth / 2, p_topRollerHeight / 2, p_topRollerHeight * 0.4, p_rollerColor);
// 3. Draw Bottom Roller
ctx.fillStyle = p_rollerColor;
ctx.fillRect(0, canvas.height - p_bottomRollerHeight, canvas.width, p_bottomRollerHeight);
// Shading for 3D effect on roller body
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.fillRect(0, canvas.height - p_bottomRollerHeight, canvas.width, Math.min(5, p_bottomRollerHeight * 0.1));
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(0, canvas.height - Math.min(5, p_bottomRollerHeight * 0.1), canvas.width, Math.min(5, p_bottomRollerHeight * 0.1));
// Draw bottom roller end caps
_drawRollerEndCap(p_rollerEndWidth / 2, canvas.height - p_bottomRollerHeight / 2, p_bottomRollerHeight * 0.4, p_rollerColor);
_drawRollerEndCap(canvas.width - p_rollerEndWidth / 2, canvas.height - p_bottomRollerHeight / 2, p_bottomRollerHeight * 0.4, p_rollerColor);
// Coordinates for drawing the image onto the paper
const imgDrawX = paperX + p_imagePadding;
const imgDrawY = paperY + p_imagePadding;
// 4. Draw Image
ctx.drawImage(originalImg, imgDrawX, imgDrawY, imageDisplayWidth, imageDisplayHeight);
// 5. Draw Title (Vertical, to the right of the image)
let lastTitleCharBaselineY = 0;
if (p_titleText) {
ctx.font = p_titleFont;
ctx.fillStyle = p_titleColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'alphabetic'; // Default, good for vertical stacking by baseline
const titleChars = p_titleText.split('');
const titleColumnX = imgDrawX + imageDisplayWidth + p_imagePadding + (titleAreaRequiredWidth - p_imagePadding / 2); // Center in allocated title area
let firstCharBaselineY = imgDrawY + titleFontSize;
if (titleActualRenderHeight < imageDisplayHeight) { // Center title vertically against image height if shorter
firstCharBaselineY = imgDrawY + (imageDisplayHeight - titleActualRenderHeight) / 2 + titleFontSize;
}
// Ensure title starts within the paper's padded area
firstCharBaselineY = Math.max(firstCharBaselineY, paperY + p_imagePadding + titleFontSize);
let currentY = firstCharBaselineY;
for (let i = 0; i < titleChars.length; i++) {
ctx.fillText(titleChars[i], titleColumnX, currentY);
lastTitleCharBaselineY = currentY;
currentY += titleFontSize + p_titleVerticalGap;
}
}
// 6. Draw Seal
if (p_sealText) {
let sealX, sealY;
// Default position: bottom right of the image, potentially overlapping slightly
sealX = imgDrawX + imageDisplayWidth - p_sealSize - 5; // 5px offset from image edge
sealY = imgDrawY + imageDisplayHeight - p_sealSize - 5; // 5px offset
if (p_titleText && lastTitleCharBaselineY > 0) {
// If title exists, place seal below the title column
const titleColumnX = imgDrawX + imageDisplayWidth + p_imagePadding + (titleAreaRequiredWidth - p_imagePadding / 2);
sealX = titleColumnX - p_sealSize / 2; // Center seal horizontally with title column
sealY = lastTitleCharBaselineY + titleFontSize * 0.2 + 10; // Below last title char + small gap
}
// Clamp seal position to be mostly within the paper boundaries
sealX = Math.max(paperX + p_imagePadding, Math.min(sealX, paperX + paperDrawableWidth - p_imagePadding - p_sealSize));
sealY = Math.max(paperY + p_imagePadding, Math.min(sealY, paperY + paperDrawableHeight - p_imagePadding - p_sealSize));
ctx.fillStyle = p_sealColor;
ctx.fillRect(sealX, sealY, p_sealSize, p_sealSize);
// Seal text: usually "relief" carving (陽刻), so text is paper color.
// For simplicity, use a light contrasting color or paper color.
// If paper is light, then this works. If paper is dark, text should be light.
const sealTextColor = _lightenColor(p_scrollColor,parseInt(p_scrollColor.substring(1,3),16) < 128 ? 70 : -10); // Lighter if paper dark, slightly darker if paper light
ctx.fillStyle = sealTextColor;
ctx.font = p_sealFont;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const sealChars = p_sealText.split('');
const numChars = sealChars.length;
// Layout seal characters (traditional Chinese: columns right-to-left, text top-to-bottom)
if (numChars === 1) {
ctx.fillText(sealChars[0], sealX + p_sealSize / 2, sealY + p_sealSize / 2);
} else if (numChars === 2) { // Two chars, arranged vertically (top char is first)
ctx.fillText(sealChars[0], sealX + p_sealSize / 2, sealY + p_sealSize * 0.30);
ctx.fillText(sealChars[1], sealX + p_sealSize / 2, sealY + p_sealSize * 0.70);
} else if (numChars === 3) { // Three chars: 1 right col, 2 left col OR 1 top, 2 bottom.
// Using 1 top center, 2 bottom R/L (less trad, but fits square)
ctx.fillText(sealChars[0], sealX + p_sealSize / 2, sealY + p_sealSize * 0.30); // Top Center
ctx.fillText(sealChars[1], sealX + p_sealSize * 0.30, sealY + p_sealSize * 0.70); // Bottom Left
ctx.fillText(sealChars[2], sealX + p_sealSize * 0.70, sealY + p_sealSize * 0.70); // Bottom Right
} else if (numChars >= 4) { // Four chars: 2x2 grid. Traditional: TopRight, TopLeft, BottomRight, BottomLeft
ctx.fillText(sealChars[0], sealX + p_sealSize * 0.70, sealY + p_sealSize * 0.30); // Top-Right
ctx.fillText(sealChars[1], sealX + p_sealSize * 0.30, sealY + p_sealSize * 0.30); // Top-Left
if (numChars > 2) ctx.fillText(sealChars[2], sealX + p_sealSize * 0.70, sealY + p_sealSize * 0.70); // Bottom-Right
if (numChars > 3) ctx.fillText(sealChars[3], sealX + p_sealSize * 0.30, sealY + p_sealSize * 0.70); // Bottom-Left
}
// Add a thin border to the seal itself
ctx.strokeStyle = _darkenColor(p_sealColor, 30);
ctx.lineWidth = Math.max(1, p_sealSize * 0.03); // Proportional border for seal
ctx.strokeRect(sealX, sealY, p_sealSize, p_sealSize);
}
return canvas;
}
Apply Changes