Please bookmark this page to avoid losing your image tool!

Image Chinese Scroll Painting Creator

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Chinese Scroll Painting Creator allows users to transform images into traditional Chinese scroll paintings. This tool provides options to customize the appearance of the scroll, including the color of the scroll paper and wooden rollers, the addition of vertical title text, and a decorative seal. Users can easily showcase their images in an elegant format suitable for art displays, personal collections, or cultural presentations, making it ideal for artists, educators, and anyone interested in Asian art forms.

Leave a Reply

Your email address will not be published. Required fields are marked *