Please bookmark this page to avoid losing your image tool!

Image Plastic Wrap Filter

(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.
async function processImage(originalImg, bumpDepth = 10, bumpDetail = 7, highlightIntensity = 0.75, lightAngleDeg = 45, lightColorStr = "255,255,255", ambientLight = 0.2) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    // Ensure the image is loaded and has dimensions
    // The problem implies originalImg is a loaded JS Image object
    const imgWidth = originalImg.naturalWidth || originalImg.width;
    const imgHeight = originalImg.naturalHeight || originalImg.height;

    if (imgWidth === 0 || imgHeight === 0) {
        // Return an empty canvas or handle error appropriately
        canvas.width = 0;
        canvas.height = 0;
        return canvas;
    }

    canvas.width = imgWidth;
    canvas.height = imgHeight;

    // Draw original image to a temporary canvas to get pixel data
    const tempCanvas = document.createElement('canvas');
    tempCanvas.width = imgWidth;
    tempCanvas.height = imgHeight;
    const tempCtx = tempCanvas.getContext('2d');
    tempCtx.drawImage(originalImg, 0, 0);
    const originalImageData = tempCtx.getImageData(0, 0, imgWidth, imgHeight);
    const originalPixels = originalImageData.data;

    const outputImageData = ctx.createImageData(imgWidth, imgHeight);
    const outputPixels = outputImageData.data;

    // --- Parameter Parsing and Setup ---
    let lightColor = [255, 255, 255]; // Default to white
    try {
        const parts = lightColorStr.split(',').map(s => parseInt(s.trim(), 10));
        if (parts.length === 3 && parts.every(p => !isNaN(p) && p >= 0 && p <= 255)) {
            lightColor = parts;
        }
    } catch (e) {
        // Parsing failed, use default white light
        console.warn("Failed to parse lightColorStr, using default white.", e);
    }
    
    const lightColorNorm = [lightColor[0] / 255, lightColor[1] / 255, lightColor[2] / 255];
    const lightAngleRad = lightAngleDeg * Math.PI / 180;

    // --- Noise Generation Setup ---
    // bumpDetail: 1 (coarse) to 15 (fine). Inverse relationship with cell size.
    const noiseScaleFactor = Math.max(1, 16 - Math.max(1, Math.min(15, bumpDetail))); 
    const noiseGridCellSize = Math.max(4, Math.floor(Math.min(imgWidth, imgHeight) / (noiseScaleFactor * 1.5 + 5)));

    const noiseGridW = Math.ceil(imgWidth / noiseGridCellSize) + 2; // Add buffer for edge sampling
    const noiseGridH = Math.ceil(imgHeight / noiseGridCellSize) + 2;
    const noiseMap = new Float32Array(noiseGridW * noiseGridH);
    for (let i = 0; i < noiseMap.length; i++) {
        noiseMap[i] = Math.random(); // Simple random noise values [0, 1)
    }

    // --- Helper Functions ---
    const lerp = (a, b, t) => a * (1 - t) + b * t;
    const s_curve = (t) => t * t * (3.0 - 2.0 * t); // Smoothstep function

    const getHeight = (imgX, imgY) => {
        const x = imgX / noiseGridCellSize; // Convert image coordinates to noise grid coordinates
        const y = imgY / noiseGridCellSize;

        const gxi0 = Math.floor(x); // Integer part of grid coordinate
        const gyi0 = Math.floor(y);
        
        const tx = s_curve(x - gxi0); // Fractional part for interpolation, smoothed
        const ty = s_curve(y - gyi0);

        // Access noiseMap, clamping indices to be within bounds
        const C = (ix, iy) => {
            const clampedIx = Math.max(0, Math.min(ix, noiseGridW - 1));
            const clampedIy = Math.max(0, Math.min(iy, noiseGridH - 1));
            return noiseMap[clampedIy * noiseGridW + clampedIx];
        };

        const p00 = C(gxi0,     gyi0); // Value at grid point (i, j)
        const p10 = C(gxi0 + 1, gyi0); // Value at grid point (i+1, j)
        const p01 = C(gxi0,     gyi0 + 1); // Value at grid point (i, j+1)
        const p11 = C(gxi0 + 1, gyi0 + 1); // Value at grid point (i+1, j+1)
        
        // Bilinear interpolation
        return lerp(lerp(p00, p10, tx), lerp(p01, p11, tx), ty);
    };
    
    const normalizeVec3 = (v) => {
        const len = Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
        if (len === 0) return [0, 0, 1]; // Default to a normal pointing straight out (Z-axis)
        return [v[0]/len, v[1]/len, v[2]/len];
    };

    const dotVec3 = (v1, v2) => v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2];

    // --- Lighting Constants ---
    const diffusionFactor = 0.7; // How much diffuse light contributes to surface color
    // Shininess: higher value = sharper, smaller highlights
    const shininessExponent = 10 + (Math.max(0, Math.min(1, highlightIntensity)) * 60); 
    const lightVecZ = 0.5; // Z-component of light vector (0=horizontal, 1=directly overhead)

    // --- Main Pixel Loop ---
    for (let y = 0; y < imgHeight; y++) {
        for (let x = 0; x < imgWidth; x++) {
            // Calculate height map gradients for normal vector and displacement
            const hL = getHeight(x - 1, y); // Height at (x-1, y)
            const hR = getHeight(x + 1, y); // Height at (x+1, y)
            const hU = getHeight(x, y - 1); // Height at (x, y-1)
            const hD = getHeight(x, y + 1); // Height at (x, y+1)

            // Gradients represent the slope of the height map
            const gradX = (hR - hL); // Change in height along X
            const gradY = (hD - hU); // Change in height along Y

            // 1. Calculate Displacement
            // Scale displacement by bumpDepth; 0.5 is an empirical factor for sensible displacement
            const displacementX = gradX * bumpDepth * 0.5;
            const displacementY = gradY * bumpDepth * 0.5;

            // Determine source pixel coordinates after displacement, clamped to image bounds
            const srcX = Math.max(0, Math.min(imgWidth - 1, Math.round(x + displacementX)));
            const srcY = Math.max(0, Math.min(imgHeight - 1, Math.round(y + displacementY)));
            const srcIdx = (srcY * imgWidth + srcX) * 4;
            
            const rOrig = originalPixels[srcIdx];
            const gOrig = originalPixels[srcIdx + 1];
            const bOrig = originalPixels[srcIdx + 2];
            const aOrig = originalPixels[srcIdx + 3];

            // 2. Calculate Lighting Effects
            // Surface Normal Vector (N)
            // normalZStrength determines "flatness" of the surface. Higher bumpDepth -> smaller normalZ -> steeper surface.
            const normalZStrength = Math.max(0.01, 2.5 / Math.max(1, bumpDepth)); 
            const N = normalizeVec3([-gradX, -gradY, normalZStrength]); 

            // Light Vector (L)
            const L = normalizeVec3([Math.cos(lightAngleRad), Math.sin(lightAngleRad), lightVecZ]);
            
            // View Vector (V) - assuming viewer is looking straight at the surface (along Z-axis)
            const V = [0, 0, 1];

            // Diffuse Reflection (Lambertian model)
            const diffuse = Math.max(0, dotVec3(N, L));

            // Specular Reflection (Blinn-Phong model using Halfway Vector)
            const Hx = L[0] + V[0]; 
            const Hy = L[1] + V[1]; 
            const Hz = L[2] + V[2];
            const H = normalizeVec3([Hx, Hy, Hz]); // Halfway vector
            const specAngle = Math.max(0, dotVec3(N, H));
            const specular = Math.pow(specAngle, shininessExponent) * highlightIntensity;

            // Combine lighting components
            // Base color from original image, modulated by ambient and diffuse light
            // Specular highlights are additive and colored by the light source
            let r = rOrig * (ambientLight + diffuse * diffusionFactor) + specular * lightColorNorm[0] * 255;
            let g = gOrig * (ambientLight + diffuse * diffusionFactor) + specular * lightColorNorm[1] * 255;
            let b = bOrig * (ambientLight + diffuse * diffusionFactor) + specular * lightColorNorm[2] * 255;
            
            // Write final pixel color to output image data, clamping values
            const currentOutputIdx = (y * imgWidth + x) * 4;
            outputPixels[currentOutputIdx]     = Math.max(0, Math.min(255, r));
            outputPixels[currentOutputIdx + 1] = Math.max(0, Math.min(255, g));
            outputPixels[currentOutputIdx + 2] = Math.max(0, Math.min(255, b));
            outputPixels[currentOutputIdx + 3] = aOrig; // Preserve original alpha
        }
    }

    ctx.putImageData(outputImageData, 0, 0);
    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 Plastic Wrap Filter is a tool designed to apply a realistic plastic wrap effect to images. By simulating the properties of plastic wrap, it alters the image’s texture and appearance, creating a three-dimensional effect that enhances the visual depth. This tool allows users to customize the bump depth, bump detail, highlight intensity, light angle, and light color, giving flexibility in achieving the desired level of realism. Use cases for this filter include enhancing product photos, creating artistic effects for social media posts, or simply adding a unique touch to personal images.

Leave a Reply

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