Results 1 to 1 of 1

Thread: Chroma Key effect in pure Javascript

  1. #1

    Thread Starter
    Fanatic Member Peter Porter's Avatar
    Join Date
    Jul 2013
    Location
    Germany
    Posts
    562

    Chroma Key effect in pure Javascript

    I was inspired by Boops boops' Visual Basic Chroma Key code years ago, but I wasn't happy with my Javascript version then because it was too slow. Now, with the help of ChatGPT, it runs pretty fast after some tweaks.

    You can try it online at: https://jsfiddle.net/PeterPorter/74deLy2f/3/
    If you see nothing visiting the link above, click "Run" at the top of that link's page. You can only load images from your PC to test it.



    Simply use Notepad to save the codes below to the same folder, then click "html.index" to launch it in your browser. When saving with Notepad, make sure to set "Save as Type" to "All FIles (*.*)" and "Encoding" to "UTF-8".

    index.html
    Code:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Chroma Key App</title>
        <link rel="stylesheet" href="styles.css"> <!-- Link to CSS file -->
    </head>
    <body>
        <h1>Chroma Keyer</h1>
        <div id="layoutContainer">
            <!-- Button container -->
            <div id="buttonContainer">
                <button id="loadForeground">Load Foreground Image</button>
                <button id="loadBackground">Load Background Image</button>
            </div>
    
            <!-- Vertical divider -->
            <div id="divider"></div>
    
            <!-- Slider container -->
            <div id="sliderContainer">
                <div class="sliderRow">
                    <label for="transparencySlider1">Transparency Threshold:</label>
                    <input type="range" id="transparencySlider1" min="0" max="355" value="100">
                    <span id="threshold1Value">100</span>
                </div>
    
                <div class="sliderRow">
                    <label for="transparencySlider2" style="padding-left: 19px;">Softness (Feathering):</label>
                    <input type="range" id="transparencySlider2" min="0" max="355" value="150" style="padding-left: -30px;">
                    <span id="threshold2Value">100</span>
                </div>
            </div>
    
                <div class="PerformaceAndSave">
                    <span id="performance"></span>
                    <br><br>
                    <button id="saveImage">Save Image</button>
                </div>
    
        </div>
        <canvas id="canvas" width="800" height="600"></canvas>
    
    
        <input type="file" id="fileInput" style="display: none;" accept="image/*">
    
        
        <script src="imageProcessing.js"></script> <!-- Image processing logic -->
      
    </body>
    </html>

    styles.css
    Code:
    body {
            font-family: Arial, sans-serif;
        }
        canvas {
            border: 1px solid black;
            display: block;
            margin: 20px 0;
        }
        #PerformaceAndSave {
            margin-top: 14px;
        }
        h1 {
            font-size: 24px; /* Adjusted title size */
            font-weight: normal;
            margin: 0; /* Remove excess spacing */
        }
        #layoutContainer {
            display: flex; /* Arrange buttons and sliders side-by-side */
            align-items: flex-start; /* Align to the top */
            margin-top: 10px;
        }
        #buttonContainer {
            margin-right: 20px; /* Add space to the right of the buttons */
        }
        #buttonContainer button {
            display: block; /* Ensure buttons stack vertically */
            margin: 10px 0; /* Add spacing between buttons */
            text-align: left; /* Align buttons to the left */
        }
        #divider {
            width: 2px;
            background-color: black;
            margin: 0 20px; /* Add space around the divider */
            height: 100%; /* Match the height of the sliders */
        }
        #sliderContainer {
            padding-top: 10px;
            display: flex;
            flex-direction: column; /* Stack sliders vertically */
            gap: 10px; /* Add spacing between sliders */
            width: 700px;
        }
    
        #performance {
            padding-bottom: 20px;
        }
    
        /* Style for each slider row (label, slider, value) */
        .sliderRow {
            display: flex; /* Align items horizontally */
            align-items: left; /* Vertically center items */
            gap: 10px; /* Add space between the label, slider, and value */
        }
    
        .sliderRow label {
            margin: 0; /* Remove default margin for labels */
            align: right;
        }
    
        .sliderRow input {
            width: 400px; /* Make the slider take available space */
        }

    imageProcessing.js
    Code:
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');
    const fileInput = document.getElementById('fileInput');
    const loadForegroundBtn = document.getElementById('loadForeground');
    const loadBackgroundBtn = document.getElementById('loadBackground');
    const saveImageBtn = document.getElementById('saveImage');
    const transparencySlider1 = document.getElementById('transparencySlider1');
    const transparencySlider2 = document.getElementById('transparencySlider2');
    const threshold1Value = document.getElementById('threshold1Value');
    const threshold2Value = document.getElementById('threshold2Value');
    const performanceText = document.getElementById('performance');
    
    let foregroundImage = null;
    let backgroundImage = null;
    let alphaMap = Array(510).fill(255);
    
    // Create an offscreen canvas for isolating the foreground image
    const offscreenCanvas = document.createElement('canvas');
    const offscreenCtx = offscreenCanvas.getContext('2d');
    
    // Initialize the alpha map for transparency effects
    function initAlphaMap(fullyTransparentEndIndex, semiTransparentEndIndex) {
        alphaMap = Array(510).fill(255);
        for (let i = fullyTransparentEndIndex; i < semiTransparentEndIndex; i++) {
            const lengthOfSemis = semiTransparentEndIndex - fullyTransparentEndIndex;
            const indexValue = i - fullyTransparentEndIndex;
            const multiplier = 1 - (indexValue / lengthOfSemis);
            alphaMap[i] = Math.round(multiplier * 255);
        }
        for (let i = semiTransparentEndIndex; i < 510; i++) {
            alphaMap[i] = 0;
        }
    }
    
    // Load image files for foreground or background
    function loadImage(isForeground) {
        fileInput.click();
    
        fileInput.onchange = function () {
            const file = fileInput.files[0];
            if (!file) return;
    
            const img = new Image();
            const reader = new FileReader();
            reader.onload = function (e) {
                img.src = e.target.result;
            };
            reader.readAsDataURL(file);
    
            img.onload = function () {
                if (isForeground) {
                    foregroundImage = img;
                    resizeCanvasToFitWindow(foregroundImage);
                    applyChromaKey();
                } else {
                    backgroundImage = img;
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);
                    if (foregroundImage) {
                        applyChromaKey();
                    }
                }
            };
        };
    }
    
    function resizeCanvasToFitWindow(image) {
        const windowWidth = window.innerWidth;
        const windowHeight = window.innerHeight;
        const aspectRatio = image.width / image.height;
    
        let newWidth = windowWidth;
        let newHeight = windowWidth / aspectRatio;
    
        if (newHeight > windowHeight) {
            newHeight = windowHeight;
            newWidth = windowHeight * aspectRatio;
        }
    
        canvas.width = newWidth;
        canvas.height = newHeight;
    
        offscreenCanvas.width = newWidth;
        offscreenCanvas.height = newHeight;
    }
    
    // Apply chroma keying with improvements
    function applyChromaKey() {
        if (!foregroundImage) return;
    
        // Clear the main canvas and draw the background image
        clearCanvas();
        if (backgroundImage) {
            ctx.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);
        }
    
        // Draw the foreground image onto the offscreen canvas
        offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
        offscreenCtx.drawImage(foregroundImage, 0, 0, offscreenCanvas.width, offscreenCanvas.height);
    
        const imageData = offscreenCtx.getImageData(0, 0, offscreenCanvas.width, offscreenCanvas.height);
        const data = imageData.data;
    
        const startTime = performance.now();
    
        // First pass: Apply chroma key to determine alpha values
        for (let i = 0; i < data.length; i += 4) {
            const r = data[i];
            const g = data[i + 1];
            const b = data[i + 2];
            let a = data[i + 3];
    
            // Calculate the color distance from the green color
            const greenDistance = Math.abs(g - r) + Math.abs(g - b);
    
            // Apply chroma keying based on the green distance
            if (greenDistance > 50 && g > r && g > b) {
                const alphaIndex = g * 2 - r - b;
                if (alphaIndex >= 0 && alphaIndex < alphaMap.length) {
                    a = alphaMap[alphaIndex];
                }
            }
    
            // Update alpha channel
            data[i + 3] = a;
        }
    
        // Second pass: Apply blur to smooth the alpha transitions
        applyBlur(imageData, offscreenCanvas.width, offscreenCanvas.height);
    
        // Put the modified image data back on the offscreen canvas
        offscreenCtx.putImageData(imageData, 0, 0);
    
        // Draw the processed foreground image back onto the main canvas
        ctx.drawImage(offscreenCanvas, 0, 0, canvas.width, canvas.height);
    
        const endTime = performance.now();
        const elapsed = endTime - startTime;
        const Kpixels = Math.floor((canvas.width * canvas.height) / 1000);
        performanceText.textContent = `${Kpixels} K pixels processed in ${elapsed.toFixed(2)} ms`;
    }
    
    function applyBlur(imageData, width, height) {
        const blurRadius = 1; // You can adjust this to control the blur strength
        const alphaData = imageData.data;
        const blurredAlpha = new Uint8Array(alphaData.length / 4);
    
        // Compute blurred alpha channel
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                let totalAlpha = 0;
                let pixelCount = 0;
    
                for (let dx = -blurRadius; dx <= blurRadius; dx++) {
                    for (let dy = -blurRadius; dy <= blurRadius; dy++) {
                        const nx = x + dx;
                        const ny = y + dy;
    
                        if (nx >= 0 && ny >= 0 && nx < width && ny < height) {
                            const index = (ny * width + nx) * 4 + 3;
                            totalAlpha += alphaData[index];
                            pixelCount++;
                        }
                    }
                }
    
                const blurIndex = y * width + x;
                blurredAlpha[blurIndex] = totalAlpha / pixelCount;
            }
        }
    
        // Update the alpha channel in the image data
        for (let i = 0; i < alphaData.length; i += 4) {
            const x = (i / 4) % width;
            const y = Math.floor(i / 4 / width);
            const alpha = blurredAlpha[y * width + x];
            alphaData[i + 3] = alpha;
        }
    }
    
    function applyBlur(imageData, width, height) {
        const blurRadius = 1; // You can adjust this to control the blur strength
        const alphaData = imageData.data;
        const blurredAlpha = new Uint8Array(alphaData.length / 4);
    
        // Compute blurred alpha channel
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                let totalAlpha = 0;
                let pixelCount = 0;
    
                for (let dx = -blurRadius; dx <= blurRadius; dx++) {
                    for (let dy = -blurRadius; dy <= blurRadius; dy++) {
                        const nx = x + dx;
                        const ny = y + dy;
    
                        if (nx >= 0 && ny >= 0 && nx < width && ny < height) {
                            const index = (ny * width + nx) * 4 + 3;
                            totalAlpha += alphaData[index];
                            pixelCount++;
                        }
                    }
                }
    
                const blurIndex = y * width + x;
                blurredAlpha[blurIndex] = totalAlpha / pixelCount;
            }
        }
    
        // Update the alpha channel in the image data
        for (let i = 0; i < alphaData.length; i += 4) {
            const x = (i / 4) % width;
            const y = Math.floor(i / 4 / width);
            const alpha = blurredAlpha[y * width + x];
            alphaData[i + 3] = alpha;
        }
    }
    
    function clearCanvas() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }
    
    loadForegroundBtn.addEventListener('click', () => loadImage(true));
    loadBackgroundBtn.addEventListener('click', () => loadImage(false));
    
    transparencySlider1.addEventListener('input', () => {
        let value = parseInt(transparencySlider1.value, 10);
        if (value < 100) {
            transparencySlider1.value = 100;  // Prevent the slider from moving below 100
            value = 100;  // Set the value to 100
        }
        transparencySlider1.value = value; // Update the slider position
        threshold1Value.textContent = value;
        initAlphaMap(value, transparencySlider2.value); // Update the alpha map
        applyChromaKey(); // Real-time update on slider input
    });
    
    transparencySlider2.addEventListener('input', () => {
        threshold2Value.textContent = transparencySlider2.value;
        initAlphaMap(transparencySlider1.value, transparencySlider2.value);
        applyChromaKey(); // Real-time update on slider input
    });
    
    saveImageBtn.addEventListener('click', () => {
        const dataUrl = canvas.toDataURL('image/png');
        const a = document.createElement('a');
        a.href = dataUrl;
        a.download = 'chroma_key_image.png';
        a.click();
    });
    Last edited by Peter Porter; Jan 5th, 2025 at 08:54 PM.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width