Guest User

Untitled

a guest
Aug 4th, 2025
31
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 47.12 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Image Stream Viewer - Fast</title>
  7. <style>
  8. body {
  9. font-family: Arial, sans-serif;
  10. margin: 0;
  11. padding: 10px;
  12. background-color: #f0f0f0;
  13. }
  14.  
  15. .container {
  16. display: flex;
  17. gap: 10px;
  18. height: 95vh;
  19. }
  20.  
  21. .left-panel {
  22. width: 700px;
  23. display: flex;
  24. flex-direction: column;
  25. gap: 10px;
  26. }
  27.  
  28. .right-panel {
  29. flex: 1;
  30. display: flex;
  31. flex-direction: column;
  32. gap: 10px;
  33. }
  34.  
  35. .section {
  36. border: 1px solid #ccc;
  37. border-radius: 5px;
  38. padding: 10px;
  39. background-color: white;
  40. }
  41.  
  42. .section h3 {
  43. margin: 0 0 10px 0;
  44. padding: 0;
  45. font-size: 14px;
  46. background-color: #e0e0e0;
  47. padding: 5px;
  48. margin: -10px -10px 10px -10px;
  49. border-radius: 5px 5px 0 0;
  50. }
  51.  
  52. .controls-row {
  53. display: flex;
  54. align-items: center;
  55. gap: 10px;
  56. margin-bottom: 5px;
  57. }
  58.  
  59. .controls-row label {
  60. min-width: 60px;
  61. font-size: 12px;
  62. }
  63.  
  64. input, select, button {
  65. padding: 2px 5px;
  66. font-size: 12px;
  67. }
  68.  
  69. input[type="range"] {
  70. flex: 1;
  71. }
  72.  
  73. #imageCanvas {
  74. border: 1px solid #000;
  75. background-color: black;
  76. cursor: crosshair;
  77. }
  78.  
  79. #histogramCanvas {
  80. border: 1px solid #ccc;
  81. background-color: white;
  82. }
  83.  
  84. .image-section {
  85. flex: 3;
  86. }
  87.  
  88. .histogram-section {
  89. flex: 1;
  90. }
  91.  
  92. #commandResponse {
  93. width: 100%;
  94. height: 60px;
  95. font-family: monospace;
  96. font-size: 10px;
  97. resize: vertical;
  98. }
  99.  
  100. #infoText {
  101. width: 100%;
  102. height: 300px;
  103. font-family: monospace;
  104. font-size: 10px;
  105. resize: vertical;
  106. }
  107.  
  108. .status-indicators {
  109. display: flex;
  110. gap: 20px;
  111. font-size: 12px;
  112. font-weight: bold;
  113. }
  114.  
  115. .temp-display {
  116. font-size: 14px;
  117. font-weight: bold;
  118. color: #d00;
  119. margin: 10px 0;
  120. }
  121.  
  122. .range-controls {
  123. display: flex;
  124. align-items: center;
  125. gap: 5px;
  126. }
  127.  
  128. .range-controls button {
  129. width: 30px;
  130. padding: 2px;
  131. }
  132.  
  133. .ref-inputs {
  134. display: flex;
  135. gap: 10px;
  136. align-items: center;
  137. flex-wrap: wrap;
  138. margin-top: 10px;
  139. }
  140.  
  141. .ref-inputs label {
  142. font-size: 11px;
  143. }
  144.  
  145. .ref-inputs input {
  146. width: 60px;
  147. }
  148. </style>
  149. </head>
  150. <body>
  151. <div class="container">
  152. <div class="left-panel">
  153. <!-- Connection Section -->
  154. <div class="section">
  155. <h3>Connection</h3>
  156. <div class="controls-row">
  157. <button id="startButton">Start Stream</button>
  158. <button id="saveButton">Save</button>
  159. <label>FPS:</label>
  160. <input type="number" id="fpsInput" value="20" min="1" max="60" style="width: 50px;">
  161. </div>
  162. <div class="status-indicators">
  163. <span id="statusLabel">Status: Stopped</span>
  164. <span id="fpsLabel">Actual FPS: 0</span>
  165. </div>
  166. </div>
  167.  
  168. <!-- Display Section -->
  169. <div class="section">
  170. <h3>Display</h3>
  171. <div class="controls-row">
  172. <label>Palette:</label>
  173. <select id="paletteSelect">
  174. <option value="Grayscale">Grayscale</option>
  175. <option value="invGrayscale">invGrayscale</option>
  176. <option value="Hot">Hot</option>
  177. <option value="Jet">Jet</option>
  178. <option value="Cool">Cool</option>
  179. <option value="Viridis">Viridis</option>
  180. <option value="Plasma">Plasma</option>
  181. <option value="Inferno">Inferno</option>
  182. <option value="Rainbow">Rainbow</option>
  183. <option value="jet">Jet</option>
  184. <option value="turbo">Turbo</option>
  185. <option value="iron">Iron</option>
  186. <option value="coolwarm">Cool-Warm</option>
  187. <option value="seismic">Seismic</option>
  188. <option value="copper">Copper</option>
  189. <option value="bone">Bone</option>
  190. <option value="spring">Spring</option>
  191. <option value="summer">Summer</option>
  192. <option value="autumn">Autumn</option>
  193. <option value="winter">Winter</option>
  194.  
  195. </select>
  196. <label><input type="checkbox" id="autoRange"> Auto Range</label>
  197. </div>
  198.  
  199. <div class="controls-row">
  200. <button onclick="autoRangeNow()">Auto Now</button>
  201. <button onclick="resetRange()">Reset</button>
  202. </div>
  203.  
  204. <div class="controls-row">
  205. <label>Offset:</label>
  206. <div class="range-controls">
  207. <button onclick="adjustOffset(-5)">-</button>
  208. <input type="range" id="offsetSlider" min="7000" max="9000" value="8192">
  209. <button onclick="adjustOffset(5)">+</button>
  210. <span id="offsetLabel">8192</span>
  211. </div>
  212. </div>
  213.  
  214. <div class="controls-row">
  215. <label>Range:</label>
  216. <div class="range-controls">
  217. <button onclick="adjustRange(-5)">-</button>
  218. <input type="range" id="rangeSlider" min="1" max="1024" value="1024">
  219. <button onclick="adjustRange(5)">+</button>
  220. <span id="rangeLabel">1024</span>
  221. </div>
  222. </div>
  223.  
  224. <div class="controls-row">
  225. <label>Min:</label>
  226. <span id="minDisplay">0</span>
  227. <label>Max:</label>
  228. <span id="maxDisplay">16383</span>
  229. </div>
  230.  
  231. <div class="controls-row">
  232. <span id="rawMinLabel">0</span>
  233. <span id="rawAvgLabel">0</span>
  234. <span id="rawMaxLabel">0</span>
  235. </div>
  236. </div>
  237.  
  238. <!-- Commands Section -->
  239. <div class="section">
  240. <h3>Commands</h3>
  241. <div class="controls-row">
  242. <label>Cmd:</label>
  243. <input type="text" id="cmdInput" style="width: 100px;">
  244. <label>Value:</label>
  245. <input type="text" id="valueInput" style="width: 100px;">
  246. <button onclick="sendCommand()">Send</button>
  247. <button onclick="sendFFCCommand()">FFC</button>
  248. </div>
  249. <textarea id="commandResponse" placeholder="Command responses..."></textarea>
  250. </div>
  251.  
  252. <!-- Info Section -->
  253. <div class="section">
  254. <h3>Info</h3>
  255. <textarea id="infoText" placeholder="Log messages..."></textarea>
  256. </div>
  257. </div>
  258.  
  259. <div class="right-panel">
  260. <!-- Image Section -->
  261. <div class="section image-section">
  262. <h3>Image</h3>
  263. <canvas id="imageCanvas" width="800" height="600"></canvas>
  264. <div id="pixelLabel">Pixel: Move mouse over image</div>
  265. <div class="temp-display" id="tempLabel">Temperature: -- °C</div>
  266.  
  267. <div class="ref-inputs">
  268. <label>Ref1 °C:</label>
  269. <input type="number" id="ref1Temp" value="25.0" step="0.1">
  270. <label>Ref1 Pixel:</label>
  271. <input type="number" id="ref1Pixel" value="7750">
  272. <label>Ref2 °C:</label>
  273. <input type="number" id="ref2Temp" value="36.0" step="0.1">
  274. <label>Ref2 Pixel:</label>
  275. <input type="number" id="ref2Pixel" value="7880">
  276. </div>
  277. </div>
  278.  
  279. <!-- Histogram Section -->
  280. <div class="section histogram-section">
  281. <h3>Histogram</h3>
  282. <canvas id="histogramCanvas" width="800" height="200"></canvas>
  283. </div>
  284. </div>
  285. </div>
  286.  
  287. <script>
  288. class ImageStreamViewer {
  289. constructor() {
  290. this.frameCount = 0;
  291. this.lastFpsTime = Date.now();
  292. this.histogramSkipCounter = 0;
  293.  
  294. this.currentImageArray = null;
  295. this.currentImageData = null;
  296. this.resolution = null;
  297. this.rawData = null;
  298. this.streaming = false;
  299. this.streamInterval = null;
  300. this.lockedPixel = null;
  301. this.scaleFactorX = 1;
  302. this.scaleFactorY = 1;
  303. this.displayOffsetX = 0;
  304. this.displayOffsetY = 0;
  305.  
  306. // Get server IP dynamically
  307. this.ip = window.location.hostname;
  308. // Image format (hidden from GUI but configurable)
  309. this.width = 640;
  310. this.height = 480;
  311. this.lowOffset = 0x238;
  312. this.highOffset = 0x4B238;
  313. this.autoOffset = true;
  314.  
  315. // Dynamic offset detection
  316. this.headerPattern = new Uint8Array([0x44, 0x38, 0x58, 0x33, 0x4E, 0x54]);
  317. this.patternToImageOffset = 0xB8;
  318.  
  319. this.first = true;
  320. this.normalizedCache = null;
  321. this.cacheImageId = null;
  322. this.cacheMin = null;
  323. this.cacheMax = null;
  324.  
  325. this.setupEventListeners();
  326. this.loadSettings();
  327. }
  328.  
  329. setupEventListeners() {
  330. document.getElementById('startButton').onclick = () => this.toggleStream();
  331. document.getElementById('saveButton').onclick = () => this.saveImage();
  332. document.getElementById('paletteSelect').onchange = () => this.updateDisplay();
  333. document.getElementById('offsetSlider').oninput = (e) => this.onOffsetChange(e.target.value);
  334. document.getElementById('rangeSlider').oninput = (e) => this.onRangeChange(e.target.value);
  335.  
  336. const canvas = document.getElementById('imageCanvas');
  337. canvas.onclick = (e) => this.onCanvasClick(e);
  338. canvas.oncontextmenu = (e) => { e.preventDefault(); this.onCanvasRightClick(e); };
  339. canvas.onmousemove = (e) => this.onMouseMove(e);
  340.  
  341. document.getElementById('cmdInput').onkeypress = (e) => {
  342. if (e.key === 'Enter') this.sendCommand();
  343. };
  344. document.getElementById('valueInput').onkeypress = (e) => {
  345. if (e.key === 'Enter') this.sendCommand();
  346. };
  347.  
  348. window.onbeforeunload = () => this.saveSettings();
  349.  
  350. this.updateMinMaxDisplay();
  351. }
  352.  
  353. log(text) {
  354. const info = document.getElementById('infoText');
  355. info.value += new Date().toLocaleTimeString() + ': ' + text + '\n';
  356. info.scrollTop = info.scrollHeight;
  357. }
  358.  
  359. findDynamicOffset(data) {
  360. try {
  361. // Convert ArrayBuffer to Uint8Array for pattern search
  362. const uint8Data = new Uint8Array(data);
  363.  
  364. // Search for header pattern
  365. for (let i = 0; i <= uint8Data.length - this.headerPattern.length; i++) {
  366. let match = true;
  367. for (let j = 0; j < this.headerPattern.length; j++) {
  368. if (uint8Data[i + j] !== this.headerPattern[j]) {
  369. match = false;
  370. break;
  371. }
  372. }
  373. if (match) {
  374. const imageStart = i + this.patternToImageOffset;
  375. const offsetDistance = this.highOffset - this.lowOffset;
  376. return {
  377. lowOffset: imageStart,
  378. highOffset: imageStart + offsetDistance
  379. };
  380. }
  381. }
  382. return null;
  383. } catch (e) {
  384. return null;
  385. }
  386. }
  387.  
  388. async fetchData() {
  389. try {
  390. const url = `/cgi-bin/proxy.cgi/stream?Type=RAW&Source=Raw`;
  391. const response = await fetch(url, {
  392. method: 'GET',
  393. cache: 'no-cache'
  394. });
  395.  
  396. if (response.ok) {
  397. this.rawData = await response.arrayBuffer();
  398. this.processData();
  399.  
  400. // FPS calculation
  401. this.frameCount++;
  402. const currentTime = Date.now();
  403. if (currentTime - this.lastFpsTime >= 1000) {
  404. const fps = this.frameCount / ((currentTime - this.lastFpsTime) / 1000);
  405. document.getElementById('fpsLabel').textContent = `Actual FPS: ${fps.toFixed(1)}`;
  406. this.frameCount = 0;
  407. this.lastFpsTime = currentTime;
  408. }
  409. }
  410. } catch (e) {
  411. this.log(`Fetch error: ${e.message}`);
  412. }
  413. }
  414.  
  415. processData() {
  416. if (!this.rawData) return;
  417.  
  418. try {
  419. const totalPixels = this.width * this.height;
  420. let lowOffset = this.lowOffset;
  421. let highOffset = this.highOffset;
  422.  
  423. // Try dynamic offset detection if enabled
  424. if (this.autoOffset) {
  425. const dynamicOffsets = this.findDynamicOffset(this.rawData);
  426. if (dynamicOffsets) {
  427. lowOffset = dynamicOffsets.lowOffset;
  428. highOffset = dynamicOffsets.highOffset;
  429. }
  430. }
  431.  
  432. if (this.rawData.byteLength >= highOffset + totalPixels) {
  433. // Extract bytes
  434. const lowBytes = new Uint8Array(this.rawData, lowOffset, totalPixels);
  435. const highBytes = new Uint8Array(this.rawData, highOffset, totalPixels);
  436.  
  437. // Combine to 16-bit
  438. const imageData = new Uint16Array(totalPixels);
  439. for (let i = 0; i < totalPixels; i++) {
  440. imageData[i] = lowBytes[i] | (highBytes[i] << 8);
  441. }
  442.  
  443. this.currentImageArray = imageData;
  444.  
  445. // Update raw min/max info
  446. const sortedSample = Array.from(imageData.slice(0, Math.min(1000, totalPixels))).sort((a, b) => a - b);
  447. const rawMin = sortedSample[Math.floor(sortedSample.length * 0.01)];
  448. const rawMax = sortedSample[Math.floor(sortedSample.length * 0.99)];
  449. const rawAvg = Math.floor(imageData.reduce((sum, val) => sum + val, 0) / totalPixels);
  450.  
  451. document.getElementById('rawMinLabel').textContent = rawMin;
  452. document.getElementById('rawAvgLabel').textContent = rawAvg;
  453. document.getElementById('rawMaxLabel').textContent = rawMax;
  454.  
  455. // Auto range if enabled
  456. if (this.first || document.getElementById('autoRange').checked) {
  457. const offset = Math.floor((rawMin + rawMax) / 2);
  458. const range = Math.floor((rawMax - rawMin) / 2);
  459.  
  460. document.getElementById('offsetSlider').value = offset;
  461. document.getElementById('rangeSlider').value = range;
  462. this.onOffsetChange(offset);
  463. this.onRangeChange(range);
  464. this.first = false;
  465. }
  466.  
  467. this.updateDisplay();
  468. this.updateHistogram();
  469. }
  470. } catch (e) {
  471. this.log(`Process error: ${e.message}`);
  472. }
  473. }
  474.  
  475. updateDisplay() {
  476. if (!this.currentImageArray) return;
  477.  
  478. try {
  479. const canvas = document.getElementById('imageCanvas');
  480. const ctx = canvas.getContext('2d');
  481.  
  482. const minVal = this.getMinValue();
  483. const maxVal = this.getMaxValue();
  484.  
  485. // Check cache
  486. const imageId = this.currentImageArray.buffer;
  487. if (this.normalizedCache === null ||
  488. this.cacheImageId !== imageId ||
  489. this.cacheMin !== minVal ||
  490. this.cacheMax !== maxVal) {
  491.  
  492. // Normalize data
  493. const normalized = new Float32Array(this.currentImageArray.length);
  494. const range = maxVal - minVal;
  495. if (range > 0) {
  496. for (let i = 0; i < this.currentImageArray.length; i++) {
  497. normalized[i] = Math.max(0, Math.min(1, (this.currentImageArray[i] - minVal) / range));
  498. }
  499. }
  500.  
  501. this.normalizedCache = normalized;
  502. this.cacheImageId = imageId;
  503. this.cacheMin = minVal;
  504. this.cacheMax = maxVal;
  505. }
  506.  
  507. // Apply palette and create ImageData
  508. const palette = document.getElementById('paletteSelect').value;
  509. const imageData = ctx.createImageData(this.width, this.height);
  510.  
  511. for (let i = 0; i < this.normalizedCache.length; i++) {
  512. const color = this.applyPalette(this.normalizedCache[i], palette);
  513. const idx = i * 4;
  514. imageData.data[idx] = color[0]; // R
  515. imageData.data[idx + 1] = color[1]; // G
  516. imageData.data[idx + 2] = color[2]; // B
  517. imageData.data[idx + 3] = 255; // A
  518. }
  519.  
  520. // Scale and display
  521. const tempCanvas = document.createElement('canvas');
  522. tempCanvas.width = this.width;
  523. tempCanvas.height = this.height;
  524. const tempCtx = tempCanvas.getContext('2d');
  525. tempCtx.putImageData(imageData, 0, 0);
  526.  
  527. // Calculate scale
  528. const scaleX = canvas.width / this.width;
  529. const scaleY = canvas.height / this.height;
  530. const scale = Math.min(scaleX, scaleY, 3.0);
  531.  
  532. const displayWidth = this.width * scale;
  533. const displayHeight = this.height * scale;
  534. this.displayOffsetX = (canvas.width - displayWidth) / 2;
  535. this.displayOffsetY = (canvas.height - displayHeight) / 2;
  536. this.scaleFactorX = scale;
  537. this.scaleFactorY = scale;
  538.  
  539. ctx.fillStyle = 'black';
  540. ctx.fillRect(0, 0, canvas.width, canvas.height);
  541.  
  542. ctx.imageSmoothingEnabled = false;
  543. ctx.drawImage(tempCanvas, this.displayOffsetX, this.displayOffsetY, displayWidth, displayHeight);
  544.  
  545. // Draw locked pixel marker
  546. if (this.lockedPixel) {
  547. const markerX = this.lockedPixel.x * this.scaleFactorX + this.displayOffsetX;
  548. const markerY = this.lockedPixel.y * this.scaleFactorY + this.displayOffsetY;
  549.  
  550. ctx.fillStyle = 'red';
  551. ctx.font = '12px Arial';
  552. ctx.fillText('X', markerX - 4, markerY + 4);
  553.  
  554. // Show temperature for locked pixel
  555. const idx = this.lockedPixel.y * this.width + this.lockedPixel.x;
  556. const value = this.currentImageArray[idx];
  557. this.updateTemperatureDisplay(value, true);
  558. }else{
  559.  
  560. // Show temperature
  561. const idx = this.my * this.width + this.mx;
  562. const value = this.currentImageArray[idx];
  563. this.updateTemperatureDisplay(value, false);
  564. }
  565.  
  566.  
  567.  
  568. this.currentImageData = imageData;
  569.  
  570. } catch (e) {
  571. this.log(`Display error: ${e.message}`);
  572. }
  573. }
  574.  
  575. updateHistogram() {
  576. if (!this.currentImageArray) return;
  577.  
  578. // Skip histogram updates for better performance
  579. this.histogramSkipCounter++;
  580. if (this.histogramSkipCounter < 1) return;
  581. this.histogramSkipCounter = 0;
  582.  
  583. try {
  584. const canvas = document.getElementById('histogramCanvas');
  585. const ctx = canvas.getContext('2d');
  586.  
  587. ctx.clearRect(0, 0, canvas.width, canvas.height);
  588.  
  589. const minVal = this.getMinValue();
  590. const maxVal = this.getMaxValue();
  591. const range = maxVal - minVal;
  592.  
  593. if (range <= 0) return;
  594.  
  595. // Calculate histogram
  596. const binCount = Math.min(range, 256);
  597. const bins = new Array(binCount).fill(0);
  598.  
  599. for (let i = 0; i < this.currentImageArray.length; i++) {
  600. const val = this.currentImageArray[i];
  601. if (val >= minVal && val <= maxVal) {
  602. const binIdx = Math.floor((val - minVal) * (binCount - 1) / range);
  603. bins[Math.max(0, Math.min(binCount - 1, binIdx))]++;
  604. }
  605. }
  606.  
  607. // Find max count for scaling (use log scale)
  608. const maxCount = Math.max(...bins);
  609. if (maxCount === 0) return;
  610.  
  611. // Draw histogram
  612. ctx.strokeStyle = 'blue';
  613. ctx.lineWidth = 1;
  614. ctx.beginPath();
  615.  
  616. for (let i = 0; i < bins.length; i++) {
  617. const x = (i / (bins.length - 1)) * canvas.width;
  618. const logCount = bins[i] > 0 ? Math.log10(bins[i] + 1) : 0;
  619. const maxLog = Math.log10(maxCount + 1);
  620. const y = canvas.height - (logCount / maxLog) * (canvas.height - 20);
  621.  
  622. if (i === 0) {
  623. ctx.moveTo(x, y);
  624. } else {
  625. ctx.lineTo(x, y);
  626. }
  627. }
  628.  
  629. ctx.stroke();
  630.  
  631. // Mark current offset
  632. const offset = parseInt(document.getElementById('offsetSlider').value);
  633. if (offset >= minVal && offset <= maxVal) {
  634. const offsetX = ((offset - minVal) / range) * canvas.width;
  635. ctx.strokeStyle = 'red';
  636. ctx.lineWidth = 2;
  637. ctx.beginPath();
  638. ctx.moveTo(offsetX, 0);
  639. ctx.lineTo(offsetX, canvas.height);
  640. ctx.stroke();
  641. }
  642.  
  643. // Add labels
  644. ctx.fillStyle = 'black';
  645. ctx.font = '10px Arial';
  646. ctx.fillText(`${minVal}`, 5, canvas.height - 5);
  647. ctx.fillText(`${maxVal}`, canvas.width - 40, canvas.height - 5);
  648. ctx.fillText('Log Scale', 5, 15);
  649.  
  650. } catch (e) {
  651. this.log(`Histogram error: ${e.message}`);
  652. }
  653. }
  654.  
  655. applyPalette(normalizedValue, palette) {
  656. const val = Math.max(0, Math.min(1, normalizedValue));
  657.  
  658. switch (palette) {
  659. case 'Grayscale':
  660. const gray = Math.floor(val * 255);
  661. return [gray, gray, gray];
  662.  
  663. case 'invGrayscale':
  664. const gray2 = Math.floor((1 - val) * 255);
  665. return [gray2, gray2, gray2];
  666.  
  667. case 'Hot':
  668. if (val < 0.33) {
  669. return [Math.floor(val * 3 * 255), 0, 0];
  670. } else if (val < 0.66) {
  671. return [255, Math.floor((val - 0.33) * 3 * 255), 0];
  672. } else {
  673. return [255, 255, Math.floor((val - 0.66) * 3 * 255)];
  674. }
  675.  
  676. case 'Jet':
  677. if (val < 0.125) {
  678. return [0, 0, Math.floor(128 + val * 4 * 127)];
  679. } else if (val < 0.375) {
  680. return [0, Math.floor((val - 0.125) * 4 * 255), 255];
  681. } else if (val < 0.625) {
  682. return [Math.floor((val - 0.375) * 4 * 255), 255, Math.floor(255 - (val - 0.375) * 4 * 255)];
  683. } else if (val < 0.875) {
  684. return [255, Math.floor(255 - (val - 0.625) * 4 * 255), 0];
  685. } else {
  686. return [Math.floor(255 - (val - 0.875) * 4 * 127), 0, 0];
  687. }
  688.  
  689. case 'Cool':
  690. return [Math.floor(val * 255), Math.floor((1 - val) * 255), 255];
  691.  
  692. case 'Viridis':
  693. // Simplified viridis approximation
  694. const r = Math.floor(val * val * 255 * 0.4);
  695. const g = Math.floor(val * 255 * 0.8);
  696. const b = Math.floor((1 - val * 0.3) * 255 * 0.9);
  697. return [r, g, b];
  698.  
  699. case 'Plasma':
  700. // Simplified plasma approximation
  701. const pr = Math.floor((0.8 + 0.2 * Math.sin(val * Math.PI)) * 255);
  702. const pg = Math.floor(val * val * 255);
  703. const pb = Math.floor((1 - val) * 255 * 0.8);
  704. return [pr, pg, pb];
  705.  
  706. case 'Inferno':
  707. // Simplified inferno approximation
  708. const ir = Math.floor(val * 255);
  709. const ig = Math.floor(val * val * 255 * 0.7);
  710. const ib = Math.floor(val * val * val * 255 * 0.3);
  711. return [ir, ig, ib];
  712.  
  713. case 'Rainbow':
  714. const hue = val * 360;
  715. return this.hsvToRgb(hue, 1, 1);
  716. case 'jet':
  717. let jr, jg, jb;
  718. if (val < 0.25) {
  719. jr = 0;
  720. jg = 0;
  721. jb = Math.floor(128 + 127 * val * 4);
  722. } else if (val < 0.5) {
  723. jr = 0;
  724. jg = Math.floor(255 * (val - 0.25) * 4);
  725. jb = 255;
  726. } else if (val < 0.75) {
  727. jr = Math.floor(255 * (val - 0.5) * 4);
  728. jg = 255;
  729. jb = Math.floor(255 * (1 - (val - 0.5) * 4));
  730. } else {
  731. jr = 255;
  732. jg = Math.floor(255 * (1 - (val - 0.75) * 4));
  733. jb = 0;
  734. }
  735. return [jr, jg, jb];
  736.  
  737. case 'turbo':
  738. const tr = Math.floor(255 * Math.max(0, Math.min(1, 1.5 - Math.abs(2 * val - 1))));
  739. const tg = Math.floor(255 * Math.max(0, Math.min(1, 1.5 - Math.abs(2 * val - 0.5))));
  740. const tb = Math.floor(255 * Math.max(0, Math.min(1, 1.5 - Math.abs(2 * val))));
  741. return [tr, tg, tb];
  742. case 'iron':
  743. const ironr = Math.floor(255 * Math.min(1, val * 3));
  744. const irong = Math.floor(255 * Math.max(0, Math.min(1, val * 3 - 1)));
  745. const ironb = Math.floor(255 * Math.max(0, Math.min(1, val * 3 - 2)));
  746. return [ironr, irong, ironb];
  747.  
  748. case 'coolwarm':
  749. const cwr = Math.floor(255 * (0.23 + 0.77 * val));
  750. const cwg = Math.floor(255 * (0.3 + 0.7 * Math.sin(val * Math.PI)));
  751. const cwb = Math.floor(255 * (0.75 - 0.75 * val));
  752. return [cwr, cwg, cwb];
  753.  
  754. case 'seismic':
  755. let sr, sg, sb;
  756. if (val < 0.5) {
  757. sr = Math.floor(255 * val * 2);
  758. sg = Math.floor(255 * val * 2);
  759. sb = 255;
  760. } else {
  761. sr = 255;
  762. sg = Math.floor(255 * (2 - val * 2));
  763. sb = Math.floor(255 * (2 - val * 2));
  764. }
  765. return [sr, sg, sb];
  766.  
  767. case 'copper':
  768. const copr = Math.floor(255 * Math.min(1, val * 1.25));
  769. const copg = Math.floor(255 * val * 0.8);
  770. const copb = Math.floor(255 * val * 0.5);
  771. return [copr, copg, copb];
  772.  
  773. case 'bone':
  774. const boner = Math.floor(255 * (val * 0.7 + 0.3 * val * val));
  775. const boneg = Math.floor(255 * (val * 0.8 + 0.2 * val * val));
  776. const boneb = Math.floor(255 * val);
  777. return [boner, boneg, boneb];
  778.  
  779. case 'spring':
  780. return [255, Math.floor(255 * val), Math.floor(255 * (1 - val))];
  781.  
  782. case 'summer':
  783. return [Math.floor(255 * val), Math.floor(128 + 127 * val), Math.floor(255 * 0.4)];
  784.  
  785. case 'autumn':
  786. return [255, Math.floor(255 * val), 0];
  787.  
  788. case 'winter':
  789. return [0, Math.floor(255 * val), Math.floor(255 * (1 - val * 0.5))];
  790.  
  791. default:
  792. const defGray = Math.floor(val * 255);
  793. return [defGray, defGray, defGray];
  794. }
  795. }
  796.  
  797. hsvToRgb(h, s, v) {
  798. h = h / 60;
  799. const c = v * s;
  800. const x = c * (1 - Math.abs((h % 2) - 1));
  801. const m = v - c;
  802.  
  803. let r, g, b;
  804. if (h < 1) { r = c; g = x; b = 0; }
  805. else if (h < 2) { r = x; g = c; b = 0; }
  806. else if (h < 3) { r = 0; g = c; b = x; }
  807. else if (h < 4) { r = 0; g = x; b = c; }
  808. else if (h < 5) { r = x; g = 0; b = c; }
  809. else { r = c; g = 0; b = x; }
  810.  
  811. return [
  812. Math.floor((r + m) * 255),
  813. Math.floor((g + m) * 255),
  814. Math.floor((b + m) * 255)
  815. ];
  816. }
  817.  
  818. onCanvasClick(event) {
  819. if (!this.currentImageArray) return;
  820.  
  821. const rect = event.target.getBoundingClientRect();
  822. const x = Math.floor((event.clientX - rect.left - this.displayOffsetX) / this.scaleFactorX);
  823. const y = Math.floor((event.clientY - rect.top - this.displayOffsetY) / this.scaleFactorY);
  824.  
  825. if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
  826. this.lockedPixel = { x, y };
  827. this.updateDisplay();
  828. }
  829. }
  830.  
  831. onCanvasRightClick(event) {
  832. this.lockedPixel = null;
  833. this.updateDisplay();
  834. }
  835.  
  836. onMouseMove(event) {
  837. if (!this.currentImageArray) return;
  838.  
  839. const rect = event.target.getBoundingClientRect();
  840. const x = Math.floor((event.clientX - rect.left - this.displayOffsetX) / this.scaleFactorX);
  841. const y = Math.floor((event.clientY - rect.top - this.displayOffsetY) / this.scaleFactorY);
  842.  
  843. this.mx = x
  844. this.my = y
  845.  
  846. if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
  847. const idx = y * this.width + x;
  848. const value = this.currentImageArray[idx];
  849. document.getElementById('pixelLabel').textContent = `Pixel (${x},${y}): ${value}`;
  850.  
  851. if (!this.lockedPixel) {
  852. this.updateTemperatureDisplay(value, false);
  853. }
  854. } else {
  855. if (!this.lockedPixel) {
  856. document.getElementById('tempLabel').textContent = 'Temperature: -- °C';
  857. }
  858. }
  859. }
  860.  
  861. updateTemperatureDisplay(value, isLocked) {
  862. const ref1Temp = parseFloat(document.getElementById('ref1Temp').value);
  863. const ref1Pixel = parseInt(document.getElementById('ref1Pixel').value);
  864. const ref2Temp = parseFloat(document.getElementById('ref2Temp').value);
  865. const ref2Pixel = parseInt(document.getElementById('ref2Pixel').value);
  866.  
  867. if (ref2Pixel !== ref1Pixel) {
  868. const temperature = ref1Temp + (value - ref1Pixel) * (ref2Temp - ref1Temp) / (ref2Pixel - ref1Pixel);
  869. const prefix = isLocked ? 'Temperature (locked)' : 'Temperature';
  870. document.getElementById('tempLabel').textContent = `${prefix}: ${temperature.toFixed(1)} °C`;
  871. } else {
  872. document.getElementById('tempLabel').textContent = 'Temperature: -- °C';
  873. }
  874. }
  875.  
  876. getMinValue() {
  877. const offset = parseInt(document.getElementById('offsetSlider').value);
  878. const range = parseInt(document.getElementById('rangeSlider').value);
  879. return Math.max(0, offset - range);
  880. }
  881.  
  882. getMaxValue() {
  883. const offset = parseInt(document.getElementById('offsetSlider').value);
  884. const range = parseInt(document.getElementById('rangeSlider').value);
  885. return Math.min(16383, offset + range);
  886. }
  887.  
  888. updateMinMaxDisplay() {
  889. document.getElementById('minDisplay').textContent = this.getMinValue();
  890. document.getElementById('maxDisplay').textContent = this.getMaxValue();
  891. }
  892.  
  893. onOffsetChange(value) {
  894. document.getElementById('offsetLabel').textContent = value;
  895. this.updateMinMaxDisplay();
  896. if (this.currentImageArray) {
  897. this.updateDisplay();
  898. }
  899. }
  900.  
  901. onRangeChange(value) {
  902. document.getElementById('rangeLabel').textContent = value;
  903. this.updateMinMaxDisplay();
  904. if (this.currentImageArray) {
  905. this.updateDisplay();
  906. }
  907. }
  908.  
  909. async sendCommand() {
  910. const cmd = document.getElementById('cmdInput').value.trim();
  911. const value = document.getElementById('valueInput').value.trim();
  912.  
  913. if (!cmd) return;
  914.  
  915. const command = value ? `${cmd},${value}` : `${cmd},`;
  916. const url = `/cgi-bin/dmcmd?Command=${encodeURIComponent(command)}`;
  917.  
  918. try {
  919. const response = await fetch(url, { timeout: 5000 });
  920. const text = await response.text();
  921. document.getElementById('commandResponse').value += `OK: ${text}\n`;
  922. } catch (e) {
  923. document.getElementById('commandResponse').value += `Error: ${e.message}\n`;
  924. }
  925. }
  926.  
  927. async sendFFCCommand() {
  928. const url = `/cgi-bin/dmcmd?Command=${encodeURIComponent('KBD,C')}`;
  929.  
  930. try {
  931. const response = await fetch(url, { timeout: 5000 });
  932. const text = await response.text();
  933. document.getElementById('commandResponse').value += `FFC OK: ${text}\n`;
  934. } catch (e) {
  935. document.getElementById('commandResponse').value += `FFC Error: ${e.message}\n`;
  936. }
  937. }
  938.  
  939. streamWorker() {
  940. if (this.streaming) {
  941. this.fetchData().then(() => {
  942. const fps = parseInt(document.getElementById('fpsInput').value) || 20;
  943. setTimeout(() => this.streamWorker(), 1000 / fps);
  944. });
  945. }
  946. }
  947.  
  948. toggleStream() {
  949. if (!this.streaming) {
  950. this.first = true;
  951. this.streaming = true;
  952. document.getElementById('startButton').textContent = 'Stop';
  953. document.getElementById('statusLabel').textContent = 'Status: Streaming';
  954. this.streamWorker();
  955. } else {
  956. this.streaming = false;
  957. document.getElementById('startButton').textContent = 'Start Stream';
  958. document.getElementById('statusLabel').textContent = 'Status: Stopped';
  959. }
  960. }
  961.  
  962. saveImage() {
  963. if (!this.currentImageData) {
  964. alert('No image to save.');
  965. return;
  966. }
  967.  
  968. const canvas = document.createElement('canvas');
  969. canvas.width = this.width;
  970. canvas.height = this.height;
  971. const ctx = canvas.getContext('2d');
  972. ctx.putImageData(this.currentImageData, 0, 0);
  973.  
  974. canvas.toBlob((blob) => {
  975. const url = URL.createObjectURL(blob);
  976. const a = document.createElement('a');
  977. a.href = url;
  978. a.download = `thermal_image_${new Date().getTime()}.png`;
  979. a.click();
  980. URL.revokeObjectURL(url);
  981. });
  982. }
  983.  
  984. saveSettings() {
  985. const settings = {
  986. fps: document.getElementById('fpsInput').value,
  987. palette: document.getElementById('paletteSelect').value,
  988. autoRange: document.getElementById('autoRange').checked,
  989. offset: document.getElementById('offsetSlider').value,
  990. range: document.getElementById('rangeSlider').value,
  991. ref1Temp: document.getElementById('ref1Temp').value,
  992. ref1Pixel: document.getElementById('ref1Pixel').value,
  993. ref2Temp: document.getElementById('ref2Temp').value,
  994. ref2Pixel: document.getElementById('ref2Pixel').value,
  995. };
  996. localStorage.setItem('thermalViewerSettings', JSON.stringify(settings));
  997. }
  998.  
  999. loadSettings() {
  1000. const settings = localStorage.getItem('thermalViewerSettings');
  1001. if (settings) {
  1002. try {
  1003. const parsed = JSON.parse(settings);
  1004. document.getElementById('fpsInput').value = parsed.fps || 20;
  1005. document.getElementById('paletteSelect').value = parsed.palette || 'Grayscale';
  1006. document.getElementById('autoRange').checked = parsed.autoRange || false;
  1007. document.getElementById('offsetSlider').value = parsed.offset || 8192;
  1008. document.getElementById('rangeSlider').value = parsed.range || 1024;
  1009. document.getElementById('ref1Temp').value = parsed.ref1Temp || 25.0;
  1010. document.getElementById('ref1Pixel').value = parsed.ref1Pixel || 7750;
  1011. document.getElementById('ref2Temp').value = parsed.ref2Temp || 36.0;
  1012. document.getElementById('ref2Pixel').value = parsed.ref2Pixel || 7880;
  1013.  
  1014. this.onOffsetChange(document.getElementById('offsetSlider').value);
  1015. this.onRangeChange(document.getElementById('rangeSlider').value);
  1016. } catch (e) {
  1017. this.log(`Settings load error: ${e.message}`);
  1018. }
  1019. }
  1020. }
  1021. }
  1022.  
  1023. // Global functions for inline event handlers
  1024. function adjustOffset(delta) {
  1025. const slider = document.getElementById('offsetSlider');
  1026. const newValue = Math.max(parseInt(slider.min), Math.min(parseInt(slider.max), parseInt(slider.value) + delta));
  1027. slider.value = newValue;
  1028. viewer.onOffsetChange(newValue);
  1029. }
  1030.  
  1031. function adjustRange(delta) {
  1032. const slider = document.getElementById('rangeSlider');
  1033. const newValue = Math.max(parseInt(slider.min), Math.min(parseInt(slider.max), parseInt(slider.value) + delta));
  1034. slider.value = newValue;
  1035. viewer.onRangeChange(newValue);
  1036. }
  1037.  
  1038. function autoRangeNow() {
  1039. if (viewer.currentImageArray) {
  1040. // Calculate percentiles for auto range
  1041. const data = Array.from(viewer.currentImageArray).sort((a, b) => a - b);
  1042. const minVal = data[Math.floor(data.length * 0.01)];
  1043. const maxVal = data[Math.floor(data.length * 0.99)];
  1044.  
  1045. const offset = Math.floor((minVal + maxVal) / 2);
  1046. const range = Math.floor((maxVal - minVal) / 2);
  1047.  
  1048. document.getElementById('offsetSlider').value = offset;
  1049. document.getElementById('rangeSlider').value = range;
  1050. viewer.onOffsetChange(offset);
  1051. viewer.onRangeChange(range);
  1052. }
  1053. }
  1054.  
  1055. function resetRange() {
  1056. document.getElementById('offsetSlider').value = 8192;
  1057. document.getElementById('rangeSlider').value = 8192;
  1058. viewer.onOffsetChange(8192);
  1059. viewer.onRangeChange(8192);
  1060. }
  1061.  
  1062. function sendCommand() {
  1063. viewer.sendCommand();
  1064. }
  1065.  
  1066. function sendFFCCommand() {
  1067. viewer.sendFFCCommand();
  1068. }
  1069.  
  1070. // Initialize the application
  1071. let viewer;
  1072. document.addEventListener('DOMContentLoaded', () => {
  1073. viewer = new ImageStreamViewer();
  1074. });
  1075. </script>
  1076. </body>
  1077. </html>Ø
Add Comment
Please, Sign In to add comment