Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Image Stream Viewer - Fast</title>
- <style>
- body {
- font-family: Arial, sans-serif;
- margin: 0;
- padding: 10px;
- background-color: #f0f0f0;
- }
- .container {
- display: flex;
- gap: 10px;
- height: 95vh;
- }
- .left-panel {
- width: 700px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .right-panel {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .section {
- border: 1px solid #ccc;
- border-radius: 5px;
- padding: 10px;
- background-color: white;
- }
- .section h3 {
- margin: 0 0 10px 0;
- padding: 0;
- font-size: 14px;
- background-color: #e0e0e0;
- padding: 5px;
- margin: -10px -10px 10px -10px;
- border-radius: 5px 5px 0 0;
- }
- .controls-row {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 5px;
- }
- .controls-row label {
- min-width: 60px;
- font-size: 12px;
- }
- input, select, button {
- padding: 2px 5px;
- font-size: 12px;
- }
- input[type="range"] {
- flex: 1;
- }
- #imageCanvas {
- border: 1px solid #000;
- background-color: black;
- cursor: crosshair;
- }
- #histogramCanvas {
- border: 1px solid #ccc;
- background-color: white;
- }
- .image-section {
- flex: 3;
- }
- .histogram-section {
- flex: 1;
- }
- #commandResponse {
- width: 100%;
- height: 60px;
- font-family: monospace;
- font-size: 10px;
- resize: vertical;
- }
- #infoText {
- width: 100%;
- height: 300px;
- font-family: monospace;
- font-size: 10px;
- resize: vertical;
- }
- .status-indicators {
- display: flex;
- gap: 20px;
- font-size: 12px;
- font-weight: bold;
- }
- .temp-display {
- font-size: 14px;
- font-weight: bold;
- color: #d00;
- margin: 10px 0;
- }
- .range-controls {
- display: flex;
- align-items: center;
- gap: 5px;
- }
- .range-controls button {
- width: 30px;
- padding: 2px;
- }
- .ref-inputs {
- display: flex;
- gap: 10px;
- align-items: center;
- flex-wrap: wrap;
- margin-top: 10px;
- }
- .ref-inputs label {
- font-size: 11px;
- }
- .ref-inputs input {
- width: 60px;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="left-panel">
- <!-- Connection Section -->
- <div class="section">
- <h3>Connection</h3>
- <div class="controls-row">
- <button id="startButton">Start Stream</button>
- <button id="saveButton">Save</button>
- <label>FPS:</label>
- <input type="number" id="fpsInput" value="20" min="1" max="60" style="width: 50px;">
- </div>
- <div class="status-indicators">
- <span id="statusLabel">Status: Stopped</span>
- <span id="fpsLabel">Actual FPS: 0</span>
- </div>
- </div>
- <!-- Display Section -->
- <div class="section">
- <h3>Display</h3>
- <div class="controls-row">
- <label>Palette:</label>
- <select id="paletteSelect">
- <option value="Grayscale">Grayscale</option>
- <option value="invGrayscale">invGrayscale</option>
- <option value="Hot">Hot</option>
- <option value="Jet">Jet</option>
- <option value="Cool">Cool</option>
- <option value="Viridis">Viridis</option>
- <option value="Plasma">Plasma</option>
- <option value="Inferno">Inferno</option>
- <option value="Rainbow">Rainbow</option>
- <option value="jet">Jet</option>
- <option value="turbo">Turbo</option>
- <option value="iron">Iron</option>
- <option value="coolwarm">Cool-Warm</option>
- <option value="seismic">Seismic</option>
- <option value="copper">Copper</option>
- <option value="bone">Bone</option>
- <option value="spring">Spring</option>
- <option value="summer">Summer</option>
- <option value="autumn">Autumn</option>
- <option value="winter">Winter</option>
- </select>
- <label><input type="checkbox" id="autoRange"> Auto Range</label>
- </div>
- <div class="controls-row">
- <button onclick="autoRangeNow()">Auto Now</button>
- <button onclick="resetRange()">Reset</button>
- </div>
- <div class="controls-row">
- <label>Offset:</label>
- <div class="range-controls">
- <button onclick="adjustOffset(-5)">-</button>
- <input type="range" id="offsetSlider" min="7000" max="9000" value="8192">
- <button onclick="adjustOffset(5)">+</button>
- <span id="offsetLabel">8192</span>
- </div>
- </div>
- <div class="controls-row">
- <label>Range:</label>
- <div class="range-controls">
- <button onclick="adjustRange(-5)">-</button>
- <input type="range" id="rangeSlider" min="1" max="1024" value="1024">
- <button onclick="adjustRange(5)">+</button>
- <span id="rangeLabel">1024</span>
- </div>
- </div>
- <div class="controls-row">
- <label>Min:</label>
- <span id="minDisplay">0</span>
- <label>Max:</label>
- <span id="maxDisplay">16383</span>
- </div>
- <div class="controls-row">
- <span id="rawMinLabel">0</span>
- <span id="rawAvgLabel">0</span>
- <span id="rawMaxLabel">0</span>
- </div>
- </div>
- <!-- Commands Section -->
- <div class="section">
- <h3>Commands</h3>
- <div class="controls-row">
- <label>Cmd:</label>
- <input type="text" id="cmdInput" style="width: 100px;">
- <label>Value:</label>
- <input type="text" id="valueInput" style="width: 100px;">
- <button onclick="sendCommand()">Send</button>
- <button onclick="sendFFCCommand()">FFC</button>
- </div>
- <textarea id="commandResponse" placeholder="Command responses..."></textarea>
- </div>
- <!-- Info Section -->
- <div class="section">
- <h3>Info</h3>
- <textarea id="infoText" placeholder="Log messages..."></textarea>
- </div>
- </div>
- <div class="right-panel">
- <!-- Image Section -->
- <div class="section image-section">
- <h3>Image</h3>
- <canvas id="imageCanvas" width="800" height="600"></canvas>
- <div id="pixelLabel">Pixel: Move mouse over image</div>
- <div class="temp-display" id="tempLabel">Temperature: -- °C</div>
- <div class="ref-inputs">
- <label>Ref1 °C:</label>
- <input type="number" id="ref1Temp" value="25.0" step="0.1">
- <label>Ref1 Pixel:</label>
- <input type="number" id="ref1Pixel" value="7750">
- <label>Ref2 °C:</label>
- <input type="number" id="ref2Temp" value="36.0" step="0.1">
- <label>Ref2 Pixel:</label>
- <input type="number" id="ref2Pixel" value="7880">
- </div>
- </div>
- <!-- Histogram Section -->
- <div class="section histogram-section">
- <h3>Histogram</h3>
- <canvas id="histogramCanvas" width="800" height="200"></canvas>
- </div>
- </div>
- </div>
- <script>
- class ImageStreamViewer {
- constructor() {
- this.frameCount = 0;
- this.lastFpsTime = Date.now();
- this.histogramSkipCounter = 0;
- this.currentImageArray = null;
- this.currentImageData = null;
- this.resolution = null;
- this.rawData = null;
- this.streaming = false;
- this.streamInterval = null;
- this.lockedPixel = null;
- this.scaleFactorX = 1;
- this.scaleFactorY = 1;
- this.displayOffsetX = 0;
- this.displayOffsetY = 0;
- // Get server IP dynamically
- this.ip = window.location.hostname;
- // Image format (hidden from GUI but configurable)
- this.width = 640;
- this.height = 480;
- this.lowOffset = 0x238;
- this.highOffset = 0x4B238;
- this.autoOffset = true;
- // Dynamic offset detection
- this.headerPattern = new Uint8Array([0x44, 0x38, 0x58, 0x33, 0x4E, 0x54]);
- this.patternToImageOffset = 0xB8;
- this.first = true;
- this.normalizedCache = null;
- this.cacheImageId = null;
- this.cacheMin = null;
- this.cacheMax = null;
- this.setupEventListeners();
- this.loadSettings();
- }
- setupEventListeners() {
- document.getElementById('startButton').onclick = () => this.toggleStream();
- document.getElementById('saveButton').onclick = () => this.saveImage();
- document.getElementById('paletteSelect').onchange = () => this.updateDisplay();
- document.getElementById('offsetSlider').oninput = (e) => this.onOffsetChange(e.target.value);
- document.getElementById('rangeSlider').oninput = (e) => this.onRangeChange(e.target.value);
- const canvas = document.getElementById('imageCanvas');
- canvas.onclick = (e) => this.onCanvasClick(e);
- canvas.oncontextmenu = (e) => { e.preventDefault(); this.onCanvasRightClick(e); };
- canvas.onmousemove = (e) => this.onMouseMove(e);
- document.getElementById('cmdInput').onkeypress = (e) => {
- if (e.key === 'Enter') this.sendCommand();
- };
- document.getElementById('valueInput').onkeypress = (e) => {
- if (e.key === 'Enter') this.sendCommand();
- };
- window.onbeforeunload = () => this.saveSettings();
- this.updateMinMaxDisplay();
- }
- log(text) {
- const info = document.getElementById('infoText');
- info.value += new Date().toLocaleTimeString() + ': ' + text + '\n';
- info.scrollTop = info.scrollHeight;
- }
- findDynamicOffset(data) {
- try {
- // Convert ArrayBuffer to Uint8Array for pattern search
- const uint8Data = new Uint8Array(data);
- // Search for header pattern
- for (let i = 0; i <= uint8Data.length - this.headerPattern.length; i++) {
- let match = true;
- for (let j = 0; j < this.headerPattern.length; j++) {
- if (uint8Data[i + j] !== this.headerPattern[j]) {
- match = false;
- break;
- }
- }
- if (match) {
- const imageStart = i + this.patternToImageOffset;
- const offsetDistance = this.highOffset - this.lowOffset;
- return {
- lowOffset: imageStart,
- highOffset: imageStart + offsetDistance
- };
- }
- }
- return null;
- } catch (e) {
- return null;
- }
- }
- async fetchData() {
- try {
- const url = `/cgi-bin/proxy.cgi/stream?Type=RAW&Source=Raw`;
- const response = await fetch(url, {
- method: 'GET',
- cache: 'no-cache'
- });
- if (response.ok) {
- this.rawData = await response.arrayBuffer();
- this.processData();
- // FPS calculation
- this.frameCount++;
- const currentTime = Date.now();
- if (currentTime - this.lastFpsTime >= 1000) {
- const fps = this.frameCount / ((currentTime - this.lastFpsTime) / 1000);
- document.getElementById('fpsLabel').textContent = `Actual FPS: ${fps.toFixed(1)}`;
- this.frameCount = 0;
- this.lastFpsTime = currentTime;
- }
- }
- } catch (e) {
- this.log(`Fetch error: ${e.message}`);
- }
- }
- processData() {
- if (!this.rawData) return;
- try {
- const totalPixels = this.width * this.height;
- let lowOffset = this.lowOffset;
- let highOffset = this.highOffset;
- // Try dynamic offset detection if enabled
- if (this.autoOffset) {
- const dynamicOffsets = this.findDynamicOffset(this.rawData);
- if (dynamicOffsets) {
- lowOffset = dynamicOffsets.lowOffset;
- highOffset = dynamicOffsets.highOffset;
- }
- }
- if (this.rawData.byteLength >= highOffset + totalPixels) {
- // Extract bytes
- const lowBytes = new Uint8Array(this.rawData, lowOffset, totalPixels);
- const highBytes = new Uint8Array(this.rawData, highOffset, totalPixels);
- // Combine to 16-bit
- const imageData = new Uint16Array(totalPixels);
- for (let i = 0; i < totalPixels; i++) {
- imageData[i] = lowBytes[i] | (highBytes[i] << 8);
- }
- this.currentImageArray = imageData;
- // Update raw min/max info
- const sortedSample = Array.from(imageData.slice(0, Math.min(1000, totalPixels))).sort((a, b) => a - b);
- const rawMin = sortedSample[Math.floor(sortedSample.length * 0.01)];
- const rawMax = sortedSample[Math.floor(sortedSample.length * 0.99)];
- const rawAvg = Math.floor(imageData.reduce((sum, val) => sum + val, 0) / totalPixels);
- document.getElementById('rawMinLabel').textContent = rawMin;
- document.getElementById('rawAvgLabel').textContent = rawAvg;
- document.getElementById('rawMaxLabel').textContent = rawMax;
- // Auto range if enabled
- if (this.first || document.getElementById('autoRange').checked) {
- const offset = Math.floor((rawMin + rawMax) / 2);
- const range = Math.floor((rawMax - rawMin) / 2);
- document.getElementById('offsetSlider').value = offset;
- document.getElementById('rangeSlider').value = range;
- this.onOffsetChange(offset);
- this.onRangeChange(range);
- this.first = false;
- }
- this.updateDisplay();
- this.updateHistogram();
- }
- } catch (e) {
- this.log(`Process error: ${e.message}`);
- }
- }
- updateDisplay() {
- if (!this.currentImageArray) return;
- try {
- const canvas = document.getElementById('imageCanvas');
- const ctx = canvas.getContext('2d');
- const minVal = this.getMinValue();
- const maxVal = this.getMaxValue();
- // Check cache
- const imageId = this.currentImageArray.buffer;
- if (this.normalizedCache === null ||
- this.cacheImageId !== imageId ||
- this.cacheMin !== minVal ||
- this.cacheMax !== maxVal) {
- // Normalize data
- const normalized = new Float32Array(this.currentImageArray.length);
- const range = maxVal - minVal;
- if (range > 0) {
- for (let i = 0; i < this.currentImageArray.length; i++) {
- normalized[i] = Math.max(0, Math.min(1, (this.currentImageArray[i] - minVal) / range));
- }
- }
- this.normalizedCache = normalized;
- this.cacheImageId = imageId;
- this.cacheMin = minVal;
- this.cacheMax = maxVal;
- }
- // Apply palette and create ImageData
- const palette = document.getElementById('paletteSelect').value;
- const imageData = ctx.createImageData(this.width, this.height);
- for (let i = 0; i < this.normalizedCache.length; i++) {
- const color = this.applyPalette(this.normalizedCache[i], palette);
- const idx = i * 4;
- imageData.data[idx] = color[0]; // R
- imageData.data[idx + 1] = color[1]; // G
- imageData.data[idx + 2] = color[2]; // B
- imageData.data[idx + 3] = 255; // A
- }
- // Scale and display
- const tempCanvas = document.createElement('canvas');
- tempCanvas.width = this.width;
- tempCanvas.height = this.height;
- const tempCtx = tempCanvas.getContext('2d');
- tempCtx.putImageData(imageData, 0, 0);
- // Calculate scale
- const scaleX = canvas.width / this.width;
- const scaleY = canvas.height / this.height;
- const scale = Math.min(scaleX, scaleY, 3.0);
- const displayWidth = this.width * scale;
- const displayHeight = this.height * scale;
- this.displayOffsetX = (canvas.width - displayWidth) / 2;
- this.displayOffsetY = (canvas.height - displayHeight) / 2;
- this.scaleFactorX = scale;
- this.scaleFactorY = scale;
- ctx.fillStyle = 'black';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.imageSmoothingEnabled = false;
- ctx.drawImage(tempCanvas, this.displayOffsetX, this.displayOffsetY, displayWidth, displayHeight);
- // Draw locked pixel marker
- if (this.lockedPixel) {
- const markerX = this.lockedPixel.x * this.scaleFactorX + this.displayOffsetX;
- const markerY = this.lockedPixel.y * this.scaleFactorY + this.displayOffsetY;
- ctx.fillStyle = 'red';
- ctx.font = '12px Arial';
- ctx.fillText('X', markerX - 4, markerY + 4);
- // Show temperature for locked pixel
- const idx = this.lockedPixel.y * this.width + this.lockedPixel.x;
- const value = this.currentImageArray[idx];
- this.updateTemperatureDisplay(value, true);
- }else{
- // Show temperature
- const idx = this.my * this.width + this.mx;
- const value = this.currentImageArray[idx];
- this.updateTemperatureDisplay(value, false);
- }
- this.currentImageData = imageData;
- } catch (e) {
- this.log(`Display error: ${e.message}`);
- }
- }
- updateHistogram() {
- if (!this.currentImageArray) return;
- // Skip histogram updates for better performance
- this.histogramSkipCounter++;
- if (this.histogramSkipCounter < 1) return;
- this.histogramSkipCounter = 0;
- try {
- const canvas = document.getElementById('histogramCanvas');
- const ctx = canvas.getContext('2d');
- ctx.clearRect(0, 0, canvas.width, canvas.height);
- const minVal = this.getMinValue();
- const maxVal = this.getMaxValue();
- const range = maxVal - minVal;
- if (range <= 0) return;
- // Calculate histogram
- const binCount = Math.min(range, 256);
- const bins = new Array(binCount).fill(0);
- for (let i = 0; i < this.currentImageArray.length; i++) {
- const val = this.currentImageArray[i];
- if (val >= minVal && val <= maxVal) {
- const binIdx = Math.floor((val - minVal) * (binCount - 1) / range);
- bins[Math.max(0, Math.min(binCount - 1, binIdx))]++;
- }
- }
- // Find max count for scaling (use log scale)
- const maxCount = Math.max(...bins);
- if (maxCount === 0) return;
- // Draw histogram
- ctx.strokeStyle = 'blue';
- ctx.lineWidth = 1;
- ctx.beginPath();
- for (let i = 0; i < bins.length; i++) {
- const x = (i / (bins.length - 1)) * canvas.width;
- const logCount = bins[i] > 0 ? Math.log10(bins[i] + 1) : 0;
- const maxLog = Math.log10(maxCount + 1);
- const y = canvas.height - (logCount / maxLog) * (canvas.height - 20);
- if (i === 0) {
- ctx.moveTo(x, y);
- } else {
- ctx.lineTo(x, y);
- }
- }
- ctx.stroke();
- // Mark current offset
- const offset = parseInt(document.getElementById('offsetSlider').value);
- if (offset >= minVal && offset <= maxVal) {
- const offsetX = ((offset - minVal) / range) * canvas.width;
- ctx.strokeStyle = 'red';
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(offsetX, 0);
- ctx.lineTo(offsetX, canvas.height);
- ctx.stroke();
- }
- // Add labels
- ctx.fillStyle = 'black';
- ctx.font = '10px Arial';
- ctx.fillText(`${minVal}`, 5, canvas.height - 5);
- ctx.fillText(`${maxVal}`, canvas.width - 40, canvas.height - 5);
- ctx.fillText('Log Scale', 5, 15);
- } catch (e) {
- this.log(`Histogram error: ${e.message}`);
- }
- }
- applyPalette(normalizedValue, palette) {
- const val = Math.max(0, Math.min(1, normalizedValue));
- switch (palette) {
- case 'Grayscale':
- const gray = Math.floor(val * 255);
- return [gray, gray, gray];
- case 'invGrayscale':
- const gray2 = Math.floor((1 - val) * 255);
- return [gray2, gray2, gray2];
- case 'Hot':
- if (val < 0.33) {
- return [Math.floor(val * 3 * 255), 0, 0];
- } else if (val < 0.66) {
- return [255, Math.floor((val - 0.33) * 3 * 255), 0];
- } else {
- return [255, 255, Math.floor((val - 0.66) * 3 * 255)];
- }
- case 'Jet':
- if (val < 0.125) {
- return [0, 0, Math.floor(128 + val * 4 * 127)];
- } else if (val < 0.375) {
- return [0, Math.floor((val - 0.125) * 4 * 255), 255];
- } else if (val < 0.625) {
- return [Math.floor((val - 0.375) * 4 * 255), 255, Math.floor(255 - (val - 0.375) * 4 * 255)];
- } else if (val < 0.875) {
- return [255, Math.floor(255 - (val - 0.625) * 4 * 255), 0];
- } else {
- return [Math.floor(255 - (val - 0.875) * 4 * 127), 0, 0];
- }
- case 'Cool':
- return [Math.floor(val * 255), Math.floor((1 - val) * 255), 255];
- case 'Viridis':
- // Simplified viridis approximation
- const r = Math.floor(val * val * 255 * 0.4);
- const g = Math.floor(val * 255 * 0.8);
- const b = Math.floor((1 - val * 0.3) * 255 * 0.9);
- return [r, g, b];
- case 'Plasma':
- // Simplified plasma approximation
- const pr = Math.floor((0.8 + 0.2 * Math.sin(val * Math.PI)) * 255);
- const pg = Math.floor(val * val * 255);
- const pb = Math.floor((1 - val) * 255 * 0.8);
- return [pr, pg, pb];
- case 'Inferno':
- // Simplified inferno approximation
- const ir = Math.floor(val * 255);
- const ig = Math.floor(val * val * 255 * 0.7);
- const ib = Math.floor(val * val * val * 255 * 0.3);
- return [ir, ig, ib];
- case 'Rainbow':
- const hue = val * 360;
- return this.hsvToRgb(hue, 1, 1);
- case 'jet':
- let jr, jg, jb;
- if (val < 0.25) {
- jr = 0;
- jg = 0;
- jb = Math.floor(128 + 127 * val * 4);
- } else if (val < 0.5) {
- jr = 0;
- jg = Math.floor(255 * (val - 0.25) * 4);
- jb = 255;
- } else if (val < 0.75) {
- jr = Math.floor(255 * (val - 0.5) * 4);
- jg = 255;
- jb = Math.floor(255 * (1 - (val - 0.5) * 4));
- } else {
- jr = 255;
- jg = Math.floor(255 * (1 - (val - 0.75) * 4));
- jb = 0;
- }
- return [jr, jg, jb];
- case 'turbo':
- const tr = Math.floor(255 * Math.max(0, Math.min(1, 1.5 - Math.abs(2 * val - 1))));
- const tg = Math.floor(255 * Math.max(0, Math.min(1, 1.5 - Math.abs(2 * val - 0.5))));
- const tb = Math.floor(255 * Math.max(0, Math.min(1, 1.5 - Math.abs(2 * val))));
- return [tr, tg, tb];
- case 'iron':
- const ironr = Math.floor(255 * Math.min(1, val * 3));
- const irong = Math.floor(255 * Math.max(0, Math.min(1, val * 3 - 1)));
- const ironb = Math.floor(255 * Math.max(0, Math.min(1, val * 3 - 2)));
- return [ironr, irong, ironb];
- case 'coolwarm':
- const cwr = Math.floor(255 * (0.23 + 0.77 * val));
- const cwg = Math.floor(255 * (0.3 + 0.7 * Math.sin(val * Math.PI)));
- const cwb = Math.floor(255 * (0.75 - 0.75 * val));
- return [cwr, cwg, cwb];
- case 'seismic':
- let sr, sg, sb;
- if (val < 0.5) {
- sr = Math.floor(255 * val * 2);
- sg = Math.floor(255 * val * 2);
- sb = 255;
- } else {
- sr = 255;
- sg = Math.floor(255 * (2 - val * 2));
- sb = Math.floor(255 * (2 - val * 2));
- }
- return [sr, sg, sb];
- case 'copper':
- const copr = Math.floor(255 * Math.min(1, val * 1.25));
- const copg = Math.floor(255 * val * 0.8);
- const copb = Math.floor(255 * val * 0.5);
- return [copr, copg, copb];
- case 'bone':
- const boner = Math.floor(255 * (val * 0.7 + 0.3 * val * val));
- const boneg = Math.floor(255 * (val * 0.8 + 0.2 * val * val));
- const boneb = Math.floor(255 * val);
- return [boner, boneg, boneb];
- case 'spring':
- return [255, Math.floor(255 * val), Math.floor(255 * (1 - val))];
- case 'summer':
- return [Math.floor(255 * val), Math.floor(128 + 127 * val), Math.floor(255 * 0.4)];
- case 'autumn':
- return [255, Math.floor(255 * val), 0];
- case 'winter':
- return [0, Math.floor(255 * val), Math.floor(255 * (1 - val * 0.5))];
- default:
- const defGray = Math.floor(val * 255);
- return [defGray, defGray, defGray];
- }
- }
- hsvToRgb(h, s, v) {
- h = h / 60;
- const c = v * s;
- const x = c * (1 - Math.abs((h % 2) - 1));
- const m = v - c;
- let r, g, b;
- if (h < 1) { r = c; g = x; b = 0; }
- else if (h < 2) { r = x; g = c; b = 0; }
- else if (h < 3) { r = 0; g = c; b = x; }
- else if (h < 4) { r = 0; g = x; b = c; }
- else if (h < 5) { r = x; g = 0; b = c; }
- else { r = c; g = 0; b = x; }
- return [
- Math.floor((r + m) * 255),
- Math.floor((g + m) * 255),
- Math.floor((b + m) * 255)
- ];
- }
- onCanvasClick(event) {
- if (!this.currentImageArray) return;
- const rect = event.target.getBoundingClientRect();
- const x = Math.floor((event.clientX - rect.left - this.displayOffsetX) / this.scaleFactorX);
- const y = Math.floor((event.clientY - rect.top - this.displayOffsetY) / this.scaleFactorY);
- if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
- this.lockedPixel = { x, y };
- this.updateDisplay();
- }
- }
- onCanvasRightClick(event) {
- this.lockedPixel = null;
- this.updateDisplay();
- }
- onMouseMove(event) {
- if (!this.currentImageArray) return;
- const rect = event.target.getBoundingClientRect();
- const x = Math.floor((event.clientX - rect.left - this.displayOffsetX) / this.scaleFactorX);
- const y = Math.floor((event.clientY - rect.top - this.displayOffsetY) / this.scaleFactorY);
- this.mx = x
- this.my = y
- if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
- const idx = y * this.width + x;
- const value = this.currentImageArray[idx];
- document.getElementById('pixelLabel').textContent = `Pixel (${x},${y}): ${value}`;
- if (!this.lockedPixel) {
- this.updateTemperatureDisplay(value, false);
- }
- } else {
- if (!this.lockedPixel) {
- document.getElementById('tempLabel').textContent = 'Temperature: -- °C';
- }
- }
- }
- updateTemperatureDisplay(value, isLocked) {
- const ref1Temp = parseFloat(document.getElementById('ref1Temp').value);
- const ref1Pixel = parseInt(document.getElementById('ref1Pixel').value);
- const ref2Temp = parseFloat(document.getElementById('ref2Temp').value);
- const ref2Pixel = parseInt(document.getElementById('ref2Pixel').value);
- if (ref2Pixel !== ref1Pixel) {
- const temperature = ref1Temp + (value - ref1Pixel) * (ref2Temp - ref1Temp) / (ref2Pixel - ref1Pixel);
- const prefix = isLocked ? 'Temperature (locked)' : 'Temperature';
- document.getElementById('tempLabel').textContent = `${prefix}: ${temperature.toFixed(1)} °C`;
- } else {
- document.getElementById('tempLabel').textContent = 'Temperature: -- °C';
- }
- }
- getMinValue() {
- const offset = parseInt(document.getElementById('offsetSlider').value);
- const range = parseInt(document.getElementById('rangeSlider').value);
- return Math.max(0, offset - range);
- }
- getMaxValue() {
- const offset = parseInt(document.getElementById('offsetSlider').value);
- const range = parseInt(document.getElementById('rangeSlider').value);
- return Math.min(16383, offset + range);
- }
- updateMinMaxDisplay() {
- document.getElementById('minDisplay').textContent = this.getMinValue();
- document.getElementById('maxDisplay').textContent = this.getMaxValue();
- }
- onOffsetChange(value) {
- document.getElementById('offsetLabel').textContent = value;
- this.updateMinMaxDisplay();
- if (this.currentImageArray) {
- this.updateDisplay();
- }
- }
- onRangeChange(value) {
- document.getElementById('rangeLabel').textContent = value;
- this.updateMinMaxDisplay();
- if (this.currentImageArray) {
- this.updateDisplay();
- }
- }
- async sendCommand() {
- const cmd = document.getElementById('cmdInput').value.trim();
- const value = document.getElementById('valueInput').value.trim();
- if (!cmd) return;
- const command = value ? `${cmd},${value}` : `${cmd},`;
- const url = `/cgi-bin/dmcmd?Command=${encodeURIComponent(command)}`;
- try {
- const response = await fetch(url, { timeout: 5000 });
- const text = await response.text();
- document.getElementById('commandResponse').value += `OK: ${text}\n`;
- } catch (e) {
- document.getElementById('commandResponse').value += `Error: ${e.message}\n`;
- }
- }
- async sendFFCCommand() {
- const url = `/cgi-bin/dmcmd?Command=${encodeURIComponent('KBD,C')}`;
- try {
- const response = await fetch(url, { timeout: 5000 });
- const text = await response.text();
- document.getElementById('commandResponse').value += `FFC OK: ${text}\n`;
- } catch (e) {
- document.getElementById('commandResponse').value += `FFC Error: ${e.message}\n`;
- }
- }
- streamWorker() {
- if (this.streaming) {
- this.fetchData().then(() => {
- const fps = parseInt(document.getElementById('fpsInput').value) || 20;
- setTimeout(() => this.streamWorker(), 1000 / fps);
- });
- }
- }
- toggleStream() {
- if (!this.streaming) {
- this.first = true;
- this.streaming = true;
- document.getElementById('startButton').textContent = 'Stop';
- document.getElementById('statusLabel').textContent = 'Status: Streaming';
- this.streamWorker();
- } else {
- this.streaming = false;
- document.getElementById('startButton').textContent = 'Start Stream';
- document.getElementById('statusLabel').textContent = 'Status: Stopped';
- }
- }
- saveImage() {
- if (!this.currentImageData) {
- alert('No image to save.');
- return;
- }
- const canvas = document.createElement('canvas');
- canvas.width = this.width;
- canvas.height = this.height;
- const ctx = canvas.getContext('2d');
- ctx.putImageData(this.currentImageData, 0, 0);
- canvas.toBlob((blob) => {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `thermal_image_${new Date().getTime()}.png`;
- a.click();
- URL.revokeObjectURL(url);
- });
- }
- saveSettings() {
- const settings = {
- fps: document.getElementById('fpsInput').value,
- palette: document.getElementById('paletteSelect').value,
- autoRange: document.getElementById('autoRange').checked,
- offset: document.getElementById('offsetSlider').value,
- range: document.getElementById('rangeSlider').value,
- ref1Temp: document.getElementById('ref1Temp').value,
- ref1Pixel: document.getElementById('ref1Pixel').value,
- ref2Temp: document.getElementById('ref2Temp').value,
- ref2Pixel: document.getElementById('ref2Pixel').value,
- };
- localStorage.setItem('thermalViewerSettings', JSON.stringify(settings));
- }
- loadSettings() {
- const settings = localStorage.getItem('thermalViewerSettings');
- if (settings) {
- try {
- const parsed = JSON.parse(settings);
- document.getElementById('fpsInput').value = parsed.fps || 20;
- document.getElementById('paletteSelect').value = parsed.palette || 'Grayscale';
- document.getElementById('autoRange').checked = parsed.autoRange || false;
- document.getElementById('offsetSlider').value = parsed.offset || 8192;
- document.getElementById('rangeSlider').value = parsed.range || 1024;
- document.getElementById('ref1Temp').value = parsed.ref1Temp || 25.0;
- document.getElementById('ref1Pixel').value = parsed.ref1Pixel || 7750;
- document.getElementById('ref2Temp').value = parsed.ref2Temp || 36.0;
- document.getElementById('ref2Pixel').value = parsed.ref2Pixel || 7880;
- this.onOffsetChange(document.getElementById('offsetSlider').value);
- this.onRangeChange(document.getElementById('rangeSlider').value);
- } catch (e) {
- this.log(`Settings load error: ${e.message}`);
- }
- }
- }
- }
- // Global functions for inline event handlers
- function adjustOffset(delta) {
- const slider = document.getElementById('offsetSlider');
- const newValue = Math.max(parseInt(slider.min), Math.min(parseInt(slider.max), parseInt(slider.value) + delta));
- slider.value = newValue;
- viewer.onOffsetChange(newValue);
- }
- function adjustRange(delta) {
- const slider = document.getElementById('rangeSlider');
- const newValue = Math.max(parseInt(slider.min), Math.min(parseInt(slider.max), parseInt(slider.value) + delta));
- slider.value = newValue;
- viewer.onRangeChange(newValue);
- }
- function autoRangeNow() {
- if (viewer.currentImageArray) {
- // Calculate percentiles for auto range
- const data = Array.from(viewer.currentImageArray).sort((a, b) => a - b);
- const minVal = data[Math.floor(data.length * 0.01)];
- const maxVal = data[Math.floor(data.length * 0.99)];
- const offset = Math.floor((minVal + maxVal) / 2);
- const range = Math.floor((maxVal - minVal) / 2);
- document.getElementById('offsetSlider').value = offset;
- document.getElementById('rangeSlider').value = range;
- viewer.onOffsetChange(offset);
- viewer.onRangeChange(range);
- }
- }
- function resetRange() {
- document.getElementById('offsetSlider').value = 8192;
- document.getElementById('rangeSlider').value = 8192;
- viewer.onOffsetChange(8192);
- viewer.onRangeChange(8192);
- }
- function sendCommand() {
- viewer.sendCommand();
- }
- function sendFFCCommand() {
- viewer.sendFFCCommand();
- }
- // Initialize the application
- let viewer;
- document.addEventListener('DOMContentLoaded', () => {
- viewer = new ImageStreamViewer();
- });
- </script>
- </body>
- </html>Ø
Add Comment
Please, Sign In to add comment