Skip to content

🤖 Cannot save fiddle #2453

Description

@maitreyeethakur0-prog

Error code

ERRW:ST1.0

Were you logged in?

Yes

Your username (if logged in)

No response

Your HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Software Thermal Camera</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            background-color: #121212;
            color: #ffffff;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            padding: 20px;
        }

        h1 {
            font-size: 1.8rem;
            margin-bottom: 12px;
            color: #00ffcc;
            text-transform: uppercase;
            letter-spacing: 1px;
            text-align: center;
        }

        .controls {
            margin-bottom: 15px;
            display: flex;
            gap: 10px;
            align-items: center;
            flex-wrap: wrap;
            justify-content: center;
        }

        button, select {
            background: #222;
            color: #fff;
            border: 1px solid #00ffcc;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 0.9rem;
            transition: all 0.2s ease;
        }

        button:hover, select:hover {
            background: #00ffcc;
            color: #000;
        }

        .container {
            display: flex;
            background: #1e1e1e;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.6);
            border: 1px solid #333;
            max-width: 100%;
            overflow-x: auto;
        }

        canvas {
            border-radius: 4px;
            cursor: crosshair;
            max-width: 100%;
            height: auto;
        }

        .info-panel {
            margin-top: 15px;
            font-size: 0.9rem;
            color: #aaa;
            text-align: center;
        }

        /* Hidden video element used as source */
        #videoSource {
            display: none;
        }
    </style>
</head>
<body>

    <h1>Software Thermal Camera</h1>

    <div class="controls">
        <label for="paletteSelect">Palette:</label>
        <select id="paletteSelect">
            <option value="jet">JET (Classic Thermal)</option>
            <option value="inferno">INFERNO</option>
            <option value="ironbow">IRONBOW</option>
            <option value="grayscale">GRAYSCALE</option>
        </select>
        <button id="switchCamBtn">Flip Camera</button>
        <button id="resetLockBtn">Reset Lock Probe</button>
    </div>

    <div class="container">
        <canvas id="thermalCanvas" width="780" height="480"></canvas>
    </div>

    <div class="info-panel">
        <p><strong>Instructions:</strong> Hover over the video feed to sample temperature. <strong>Click</strong> to lock a probe point.</p>
    </div>

    <video id="videoSource" autoplay playsinline></video>

    <script>
        const video = document.getElementById('videoSource');
        const canvas = document.getElementById('thermalCanvas');
        const ctx = canvas.getContext('2d');
        const paletteSelect = document.getElementById('paletteSelect');
        const resetLockBtn = document.getElementById('resetLockBtn');
        const switchCamBtn = document.getElementById('switchCamBtn');

        // Render Dimensions
        const VIEW_WIDTH = 640;
        const VIEW_HEIGHT = 480;
        const BAR_WIDTH = 30;
        const PANEL_WIDTH = 140;

        // Temperature Scale Range Config (°C)
        const TEMP_MIN = 20.0;
        const TEMP_MAX = 40.0;

        // States
        let mousePos = { x: -1, y: -1 };
        let lockedPos = null;
        let currentFacingMode = "environment"; // Defaults to rear/back camera
        let activeStream = null;

        // --- Color Palettes (256 Look-Up Table Entries) ---
        function generateLUT(paletteName) {
            const lut = new Uint8ClampedArray(256 * 3);
            for (let i = 0; i < 256; i++) {
                let v = i / 255;
                let r = 0, g = 0, b = 0;

                if (paletteName === 'jet') {
                    r = Math.min(255, Math.max(0, Math.floor(255 * (1.5 - Math.abs(v * 4 - 3)))));
                    g = Math.min(255, Math.max(0, Math.floor(255 * (1.5 - Math.abs(v * 4 - 2)))));
                    b = Math.min(255, Math.max(0, Math.floor(255 * (1.5 - Math.abs(v * 4 - 1)))));
                } else if (paletteName === 'inferno') {
                    r = Math.floor(255 * Math.pow(v, 0.7));
                    g = Math.floor(255 * Math.pow(v, 2.0));
                    b = Math.floor(255 * Math.min(1, Math.max(0, Math.sin(v * Math.PI * 1.5))));
                } else if (paletteName === 'ironbow') {
                    r = Math.floor(255 * Math.min(1, v * 1.5));
                    g = Math.floor(255 * Math.pow(v, 2));
                    b = Math.floor(255 * Math.max(0, 1 - v * 2));
                } else {
                    r = g = b = i; // Grayscale
                }

                lut[i * 3] = r;
                lut[i * 3 + 1] = g;
                lut[i * 3 + 2] = b;
            }
            return lut;
        }

        let currentLUT = generateLUT('jet');

        // Event Listeners
        paletteSelect.addEventListener('change', (e) => {
            currentLUT = generateLUT(e.target.value);
        });

        resetLockBtn.addEventListener('click', () => {
            lockedPos = null;
        });

        switchCamBtn.addEventListener('click', () => {
            currentFacingMode = (currentFacingMode === "environment") ? "user" : "environment";
            initCamera();
        });

        // Mouse Tracking
        canvas.addEventListener('mousemove', (e) => {
            const rect = canvas.getBoundingClientRect();
            mousePos.x = Math.floor((e.clientX - rect.left) * (canvas.width / rect.width));
            mousePos.y = Math.floor((e.clientY - rect.top) * (canvas.height / rect.height));
        });

        canvas.addEventListener('mouseleave', () => {
            mousePos = { x: -1, y: -1 };
        });

        canvas.addEventListener('click', () => {
            if (mousePos.x >= 0 && mousePos.x < VIEW_WIDTH && mousePos.y >= 0 && mousePos.y < VIEW_HEIGHT) {
                lockedPos = { x: mousePos.x, y: mousePos.y };
            }
        });

        // Webcam Initialization
        async function initCamera() {
            if (activeStream) {
                activeStream.getTracks().forEach(track => track.stop());
            }

            try {
                activeStream = await navigator.mediaDevices.getUserMedia({
                    video: { 
                        facingMode: { ideal: currentFacingMode },
                        width: { ideal: VIEW_WIDTH }, 
                        height: { ideal: VIEW_HEIGHT } 
                    },
                    audio: false
                });
                video.srcObject = activeStream;
                video.play();
            } catch (err) {
                console.warn("Camera Mode Fallback:", err);
                try {
                    activeStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
                    video.srcObject = activeStream;
                    video.play();
                } catch (fallbackErr) {
                    alert("Camera Access Failed: " + fallbackErr.message);
                }
            }
        }

        // Offscreen Canvas to extract video frame data
        const offscreenCanvas = document.createElement('canvas');
        offscreenCanvas.width = VIEW_WIDTH;
        offscreenCanvas.height = VIEW_HEIGHT;
        const offCtx = offscreenCanvas.getContext('2d');

        function processFrame() {
            if (video.readyState === video.HAVE_ENOUGH_DATA) {
                // 1. Capture raw frame
                offCtx.drawImage(video, 0, 0, VIEW_WIDTH, VIEW_HEIGHT);
                const frameData = offCtx.getImageData(0, 0, VIEW_WIDTH, VIEW_HEIGHT);
                const pixels = frameData.data;

                // Create heatmap pixel buffer
                const thermalImgData = ctx.createImageData(VIEW_WIDTH, VIEW_HEIGHT);
                const thermalPixels = thermalImgData.data;

                // Intensity buffer for mouse probe sampling
                const luminanceMap = new Uint8Array(VIEW_WIDTH * VIEW_HEIGHT);

                // 2. Grayscale conversion + Contrast Boost + Heatmap Mapping
                for (let i = 0; i < pixels.length; i += 4) {
                    let gray = Math.floor(0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2]);
                    
                    // Contrast enhancement
                    gray = Math.min(255, Math.max(0, Math.floor((gray - 30) * (255 / 195))));

                    luminanceMap[i / 4] = gray;

                    // Apply Color LUT
                    thermalPixels[i]     = currentLUT[gray * 3];     // Red
                    thermalPixels[i + 1] = currentLUT[gray * 3 + 1]; // Green
                    thermalPixels[i + 2] = currentLUT[gray * 3 + 2]; // Blue
                    thermalPixels[i + 3] = 255;                      // Alpha
                }

                // Render Heatmap View
                ctx.putImageData(thermalImgData, 0, 0);

                // 3. Render Side Colorbar Panel
                drawColorbarPanel();

                // 4. Render Active Probe / Reticle
                const activeProbe = lockedPos || (mousePos.x >= 0 && mousePos.x < VIEW_WIDTH ? mousePos : null);
                if (activeProbe) {
                    const px = activeProbe.x;
                    const py = activeProbe.y;
                    const idx = py * VIEW_WIDTH + px;
                    const intensity = luminanceMap[idx];
                    
                    // Linear Temperature Mapping
                    const tempC = TEMP_MIN + (intensity / 255.0) * (TEMP_MAX - TEMP_MIN);

                    // Crosshair Marker
                    ctx.strokeStyle = lockedPos ? '#00ff00' : '#ffffff';
                    ctx.lineWidth = 2;
                    ctx.beginPath();
                    ctx.moveTo(px - 10, py); ctx.lineTo(px + 10, py);
                    ctx.moveTo(px, py - 10); ctx.lineTo(px, py + 10);
                    ctx.stroke();

                    // Temperature Text Box
                    const label = `${tempC.toFixed(1)} °C`;
                    ctx.font = 'bold 14px sans-serif';
                    const textWidth = ctx.measureText(label).width;

                    const boxX = Math.min(px + 12, VIEW_WIDTH - textWidth - 15);
                    const boxY = Math.max(py - 25, 20);

                    ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
                    ctx.fillRect(boxX, boxY - 14, textWidth + 10, 20);

                    ctx.fillStyle = lockedPos ? '#00ff00' : '#ffff00';
                    ctx.fillText(label, boxX + 5, boxY);
                }
            }

            requestAnimationFrame(processFrame);
        }

        function drawColorbarPanel() {
            const startX = VIEW_WIDTH + 15;

            // Side Panel Background
            ctx.fillStyle = '#1e1e1e';
            ctx.fillRect(VIEW_WIDTH, 0, PANEL_WIDTH, VIEW_HEIGHT);

            // Vertical Gradient Scale Bar
            const barGradient = ctx.createLinearGradient(0, 0, 0, VIEW_HEIGHT);
            for (let i = 0; i <= 10; i++) {
                const ratio = i / 10;
                const lutIdx = Math.floor((1 - ratio) * 255);
                const r = currentLUT[lutIdx * 3];
                const g = currentLUT[lutIdx * 3 + 1];
                const b = currentLUT[lutIdx * 3 + 2];
                barGradient.addColorStop(ratio, `rgb(${r},${g},${b})`);
            }

            ctx.fillStyle = barGradient;
            ctx.fillRect(startX, 0, BAR_WIDTH, VIEW_HEIGHT);

            // Outer Outline for Color Bar
            ctx.strokeStyle = '#ffffff';
            ctx.lineWidth = 1;
            ctx.strokeRect(startX, 0, BAR_WIDTH, VIEW_HEIGHT);

            // Temperature Scale Ticks & Text
            const ticks = 6;
            ctx.fillStyle = '#ffffff';
            ctx.font = 'bold 13px sans-serif';
            ctx.textAlign = 'left';

            for (let i = 0; i < ticks; i++) {
                const y = (VIEW_HEIGHT / (ticks - 1)) * i;
                const tempVal = TEMP_MAX - ((TEMP_MAX - TEMP_MIN) / (ticks - 1)) * i;

                // Draw tick mark INSIDE the color bar so it can never be mistaken for a minus sign
                ctx.strokeStyle = '#ffffff';
                ctx.beginPath();
                ctx.moveTo(startX + BAR_WIDTH - 6, y);
                ctx.lineTo(startX + BAR_WIDTH, y);
                ctx.stroke();

                // Display clean text with proper spacing to the right
                const clampY = Math.max(14, Math.min(VIEW_HEIGHT - 6, y + 4));
                ctx.fillText(`${tempVal.toFixed(0)} °C`, startX + BAR_WIDTH + 12, clampY);
            }
        }

        // Initialize and Start Frame Loop
        initCamera();
        requestAnimationFrame(processFrame);
    </script>
</body>
</html>

Your JavaScript

'-'

Your CSS

'_'

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions