From e6f593b959aa108100676220c1a0611b2cbef361 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sat, 1 Aug 2026 12:08:44 +0200 Subject: [PATCH] refactor: convert screenshot support to TypeScript --- .../assets/web/generated/screenshot.js | 30 +++++---- .../trx-frontend-http/frontend/build.mjs | 2 +- .../src/{screenshot.js => screenshot.ts} | 66 ++++++++++++------- .../trx-frontend-http/frontend/tsconfig.json | 2 +- 4 files changed, 63 insertions(+), 37 deletions(-) rename src/trx-client/trx-frontend/trx-frontend-http/frontend/src/{screenshot.js => screenshot.ts} (77%) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/screenshot.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/screenshot.js index 6a22e1f5..fb75b7c5 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/screenshot.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/screenshot.js @@ -1,7 +1,8 @@ "use strict"; +const screenshotWindow = window; (function() { "use strict"; - const T = window.trx; + const T = screenshotWindow.trx; function isVisibleForSnapshot(el) { if (!el) return false; const style = getComputedStyle(el); @@ -40,25 +41,26 @@ const bgAlpha = Math.min(bg[3], maxAlpha); if (bgAlpha > 0.01) { drawRoundedRectPath(ctx, x, y, w, h, radius); - ctx.fillStyle = `rgba(${Math.round(bg[0])}, ${Math.round(bg[1])}, ${Math.round(bg[2])}, ${bgAlpha})`; + ctx.fillStyle = `rgba(${String(Math.round(bg[0]))}, ${String(Math.round(bg[1]))}, ${String(Math.round(bg[2]))}, ${String(bgAlpha)})`; ctx.fill(); } const borderAlpha = Math.min(border[3], maxAlpha); if (borderWidth > 0 && borderAlpha > 0.01) { drawRoundedRectPath(ctx, x + borderWidth * 0.5, y + borderWidth * 0.5, w - borderWidth, h - borderWidth, Math.max(0, radius - borderWidth * 0.5)); ctx.lineWidth = borderWidth; - ctx.strokeStyle = `rgba(${Math.round(border[0])}, ${Math.round(border[1])}, ${Math.round(border[2])}, ${borderAlpha})`; + ctx.strokeStyle = `rgba(${String(Math.round(border[0]))}, ${String(Math.round(border[1]))}, ${String(Math.round(border[2]))}, ${String(borderAlpha)})`; ctx.stroke(); } return { x, y, w, h, style }; } function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight, maxLines) { - const words = String(text || "").split(/\s+/).filter(Boolean); + const words = (text || "").split(/\s+/).filter(Boolean); if (!words.length) return; let line = ""; let lineIdx = 0; for (let i = 0; i < words.length; i += 1) { - const candidate = line ? `${line} ${words[i]}` : words[i]; + const word = words[i] ?? ""; + const candidate = line ? `${line} ${word}` : word; if (ctx.measureText(candidate).width <= maxWidth || !line) { line = candidate; continue; @@ -66,7 +68,7 @@ ctx.fillText(line, x, y + lineIdx * lineHeight); lineIdx += 1; if (lineIdx >= maxLines) return; - line = words[i]; + line = word; } if (line && lineIdx < maxLines) { ctx.fillText(line, x, y + lineIdx * lineHeight); @@ -132,7 +134,7 @@ try { if (typeof gl.flush === "function") gl.flush(); if (typeof gl.finish === "function") gl.finish(); - } catch (_) { + } catch { } } const rootRect = rootEl.getBoundingClientRect(); @@ -188,7 +190,9 @@ a.style.display = "none"; document.body.appendChild(a); a.click(); - requestAnimationFrame(() => a.remove()); + requestAnimationFrame(() => { + a.remove(); + }); } function saveCanvasAsPng(canvas, fileName) { if (!canvas) return Promise.resolve(false); @@ -202,10 +206,12 @@ } const url = URL.createObjectURL(blob); clickCanvasDownload(url, fileName); - setTimeout(() => URL.revokeObjectURL(url), 1e3); + setTimeout(() => { + URL.revokeObjectURL(url); + }, 1e3); resolve(true); }, "image/png"); - } catch (_) { + } catch { resolve(false); } }); @@ -213,7 +219,7 @@ try { clickCanvasDownload(canvas.toDataURL("image/png"), fileName); return Promise.resolve(true); - } catch (_) { + } catch { return Promise.resolve(false); } } @@ -228,7 +234,7 @@ T.showHint(saved ? "Spectrum screenshot saved" : "Spectrum screenshot failed", saved ? 1500 : 1800); return saved; } - window.trx.modules.screenshot = { + screenshotWindow.trx.modules.screenshot = { captureSpectrumScreenshot, buildSpectrumSnapshotCanvas, saveCanvasAsPng diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs index 565dc12c..bd20a092 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs @@ -19,7 +19,7 @@ await build({ app: path.join(sourceDir, "app.js"), "ui-core": path.join(sourceDir, "ui-core.ts"), "map-core": path.join(sourceDir, "map-core.js"), - screenshot: path.join(sourceDir, "screenshot.js"), + screenshot: path.join(sourceDir, "screenshot.ts"), "webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"), "leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.js"), ais: path.join(sourceDir, "plugins", "ais.js"), diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/screenshot.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/screenshot.ts similarity index 77% rename from src/trx-client/trx-frontend/trx-frontend-http/frontend/src/screenshot.js rename to src/trx-client/trx-frontend/trx-frontend-http/frontend/src/screenshot.ts index 7f57cc5a..5663fb1f 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/screenshot.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/screenshot.ts @@ -4,11 +4,30 @@ // Spectrum screenshot module (loaded on demand when user triggers screenshot). // Communicates with app.js core via window.trx namespace. +type Rgba255 = [number, number, number, number]; +interface SnapshotRenderer { gl?: { flush?: () => void; finish?: () => void } } +interface ScreenshotModule { + captureSpectrumScreenshot(): Promise; + buildSpectrumSnapshotCanvas(): HTMLCanvasElement | null; + saveCanvasAsPng(canvas: HTMLCanvasElement | null, fileName: string): Promise; +} +interface TrxScreenshotBridge { + cssColorToRgba(color: string): Rgba255; + overviewGl?: SnapshotRenderer; + spectrumGl?: SnapshotRenderer; + signalOverlayGl?: SnapshotRenderer; + overviewCanvas?: HTMLCanvasElement; + spectrumCanvas?: HTMLCanvasElement; + showHint(message: string, durationMs: number): void; + modules: { screenshot?: ScreenshotModule }; +} +const screenshotWindow = window as typeof window & { trx: TrxScreenshotBridge }; + (function () { "use strict"; - const T = window.trx; + const T = screenshotWindow.trx; - function isVisibleForSnapshot(el) { + function isVisibleForSnapshot(el: Element | null): el is HTMLElement { if (!el) return false; const style = getComputedStyle(el); if (style.display === "none" || style.visibility === "hidden") return false; @@ -18,7 +37,7 @@ return rect.width > 0 && rect.height > 0; } - function drawRoundedRectPath(ctx, x, y, w, h, r) { + function drawRoundedRectPath(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) { const radius = Math.max(0, Math.min(r, Math.min(w, h) / 2)); ctx.beginPath(); ctx.moveTo(x + radius, y); @@ -33,7 +52,7 @@ ctx.closePath(); } - function drawElementChrome(ctx, el, rootRect, maxAlpha = 1) { + function drawElementChrome(ctx: CanvasRenderingContext2D, el: HTMLElement | null, rootRect: DOMRect, maxAlpha = 1) { if (!isVisibleForSnapshot(el)) return null; const rect = el.getBoundingClientRect(); const style = getComputedStyle(el); @@ -49,26 +68,27 @@ const bgAlpha = Math.min(bg[3], maxAlpha); if (bgAlpha > 0.01) { drawRoundedRectPath(ctx, x, y, w, h, radius); - ctx.fillStyle = `rgba(${Math.round(bg[0])}, ${Math.round(bg[1])}, ${Math.round(bg[2])}, ${bgAlpha})`; + ctx.fillStyle = `rgba(${String(Math.round(bg[0]))}, ${String(Math.round(bg[1]))}, ${String(Math.round(bg[2]))}, ${String(bgAlpha)})`; ctx.fill(); } const borderAlpha = Math.min(border[3], maxAlpha); if (borderWidth > 0 && borderAlpha > 0.01) { drawRoundedRectPath(ctx, x + borderWidth * 0.5, y + borderWidth * 0.5, w - borderWidth, h - borderWidth, Math.max(0, radius - borderWidth * 0.5)); ctx.lineWidth = borderWidth; - ctx.strokeStyle = `rgba(${Math.round(border[0])}, ${Math.round(border[1])}, ${Math.round(border[2])}, ${borderAlpha})`; + ctx.strokeStyle = `rgba(${String(Math.round(border[0]))}, ${String(Math.round(border[1]))}, ${String(Math.round(border[2]))}, ${String(borderAlpha)})`; ctx.stroke(); } return { x, y, w, h, style }; } - function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight, maxLines) { - const words = String(text || "").split(/\s+/).filter(Boolean); + function drawWrappedText(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number, maxLines: number) { + const words = (text || "").split(/\s+/).filter(Boolean); if (!words.length) return; let line = ""; let lineIdx = 0; for (let i = 0; i < words.length; i += 1) { - const candidate = line ? `${line} ${words[i]}` : words[i]; + const word = words[i] ?? ""; + const candidate = line ? `${line} ${word}` : word; if (ctx.measureText(candidate).width <= maxWidth || !line) { line = candidate; continue; @@ -76,14 +96,14 @@ ctx.fillText(line, x, y + lineIdx * lineHeight); lineIdx += 1; if (lineIdx >= maxLines) return; - line = words[i]; + line = word; } if (line && lineIdx < maxLines) { ctx.fillText(line, x, y + lineIdx * lineHeight); } } - function drawElementTextBlock(ctx, el, rootRect, fallbackText = null, maxAlpha = 1) { + function drawElementTextBlock(ctx: CanvasRenderingContext2D, el: HTMLElement, rootRect: DOMRect, fallbackText: string | null = null, maxAlpha = 1) { const chrome = drawElementChrome(ctx, el, rootRect, maxAlpha); if (!chrome) return; const text = (fallbackText == null ? el.innerText : fallbackText) || ""; @@ -116,7 +136,7 @@ } } - function drawAxisLabels(ctx, axisEl, rootRect) { + function drawAxisLabels(ctx: CanvasRenderingContext2D, axisEl: HTMLElement | null, rootRect: DOMRect) { if (!isVisibleForSnapshot(axisEl)) return; for (const node of axisEl.children) { if (!(node instanceof HTMLElement)) continue; @@ -133,8 +153,8 @@ } } - function buildSpectrumSnapshotCanvas() { - const rootEl = document.querySelector(".signal-visual-block"); + function buildSpectrumSnapshotCanvas(): HTMLCanvasElement | null { + const rootEl = document.querySelector(".signal-visual-block"); const spectrumPanelEl = document.getElementById("spectrum-panel"); if (!rootEl || !isVisibleForSnapshot(rootEl) || !isVisibleForSnapshot(spectrumPanelEl)) { return null; @@ -145,7 +165,7 @@ try { if (typeof gl.flush === "function") gl.flush(); if (typeof gl.finish === "function") gl.finish(); - } catch (_) { + } catch { // Ignore transient WebGL state errors and capture the last good frame. } } @@ -162,7 +182,7 @@ ctx.fillStyle = bg; ctx.fillRect(0, 0, rootRect.width, rootRect.height); - const signalOverlayCanvas = document.getElementById("signal-overlay-canvas"); + const signalOverlayCanvas = document.getElementById("signal-overlay-canvas") as HTMLCanvasElement | null; const canvases = [T.overviewCanvas, T.spectrumCanvas, signalOverlayCanvas]; for (const canvas of canvases) { if (!canvas || !isVisibleForSnapshot(canvas)) continue; @@ -205,7 +225,7 @@ return out; } - function clickCanvasDownload(href, fileName) { + function clickCanvasDownload(href: string, fileName: string) { const a = document.createElement("a"); a.href = href; a.download = fileName; @@ -213,10 +233,10 @@ a.style.display = "none"; document.body.appendChild(a); a.click(); - requestAnimationFrame(() => a.remove()); + requestAnimationFrame(() => { a.remove(); }); } - function saveCanvasAsPng(canvas, fileName) { + function saveCanvasAsPng(canvas: HTMLCanvasElement | null, fileName: string): Promise { if (!canvas) return Promise.resolve(false); if (typeof canvas.toBlob === "function") { return new Promise((resolve) => { @@ -228,10 +248,10 @@ } const url = URL.createObjectURL(blob); clickCanvasDownload(url, fileName); - setTimeout(() => URL.revokeObjectURL(url), 1000); + setTimeout(() => { URL.revokeObjectURL(url); }, 1000); resolve(true); }, "image/png"); - } catch (_) { + } catch { resolve(false); } }); @@ -239,7 +259,7 @@ try { clickCanvasDownload(canvas.toDataURL("image/png"), fileName); return Promise.resolve(true); - } catch (_) { + } catch { return Promise.resolve(false); } } @@ -257,7 +277,7 @@ } // Register module API - window.trx.modules.screenshot = { + screenshotWindow.trx.modules.screenshot = { captureSpectrumScreenshot, buildSpectrumSnapshotCanvas, saveCanvasAsPng, diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tsconfig.json b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tsconfig.json index a2d9e6d4..d96bb77c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tsconfig.json +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tsconfig.json @@ -10,7 +10,7 @@ "allowJs": true, "checkJs": false, "noEmit": true, - "lib": ["ES2022", "DOM"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "types": [] }, "include": ["src/**/*.ts", "src/**/*.js"]