refactor: convert screenshot support to TypeScript
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
const screenshotWindow = window;
|
||||||
(function() {
|
(function() {
|
||||||
"use strict";
|
"use strict";
|
||||||
const T = window.trx;
|
const T = screenshotWindow.trx;
|
||||||
function isVisibleForSnapshot(el) {
|
function isVisibleForSnapshot(el) {
|
||||||
if (!el) return false;
|
if (!el) return false;
|
||||||
const style = getComputedStyle(el);
|
const style = getComputedStyle(el);
|
||||||
@@ -40,25 +41,26 @@
|
|||||||
const bgAlpha = Math.min(bg[3], maxAlpha);
|
const bgAlpha = Math.min(bg[3], maxAlpha);
|
||||||
if (bgAlpha > 0.01) {
|
if (bgAlpha > 0.01) {
|
||||||
drawRoundedRectPath(ctx, x, y, w, h, radius);
|
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();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
const borderAlpha = Math.min(border[3], maxAlpha);
|
const borderAlpha = Math.min(border[3], maxAlpha);
|
||||||
if (borderWidth > 0 && borderAlpha > 0.01) {
|
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));
|
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.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();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
return { x, y, w, h, style };
|
return { x, y, w, h, style };
|
||||||
}
|
}
|
||||||
function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight, maxLines) {
|
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;
|
if (!words.length) return;
|
||||||
let line = "";
|
let line = "";
|
||||||
let lineIdx = 0;
|
let lineIdx = 0;
|
||||||
for (let i = 0; i < words.length; i += 1) {
|
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) {
|
if (ctx.measureText(candidate).width <= maxWidth || !line) {
|
||||||
line = candidate;
|
line = candidate;
|
||||||
continue;
|
continue;
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
ctx.fillText(line, x, y + lineIdx * lineHeight);
|
ctx.fillText(line, x, y + lineIdx * lineHeight);
|
||||||
lineIdx += 1;
|
lineIdx += 1;
|
||||||
if (lineIdx >= maxLines) return;
|
if (lineIdx >= maxLines) return;
|
||||||
line = words[i];
|
line = word;
|
||||||
}
|
}
|
||||||
if (line && lineIdx < maxLines) {
|
if (line && lineIdx < maxLines) {
|
||||||
ctx.fillText(line, x, y + lineIdx * lineHeight);
|
ctx.fillText(line, x, y + lineIdx * lineHeight);
|
||||||
@@ -132,7 +134,7 @@
|
|||||||
try {
|
try {
|
||||||
if (typeof gl.flush === "function") gl.flush();
|
if (typeof gl.flush === "function") gl.flush();
|
||||||
if (typeof gl.finish === "function") gl.finish();
|
if (typeof gl.finish === "function") gl.finish();
|
||||||
} catch (_) {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rootRect = rootEl.getBoundingClientRect();
|
const rootRect = rootEl.getBoundingClientRect();
|
||||||
@@ -188,7 +190,9 @@
|
|||||||
a.style.display = "none";
|
a.style.display = "none";
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
requestAnimationFrame(() => a.remove());
|
requestAnimationFrame(() => {
|
||||||
|
a.remove();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function saveCanvasAsPng(canvas, fileName) {
|
function saveCanvasAsPng(canvas, fileName) {
|
||||||
if (!canvas) return Promise.resolve(false);
|
if (!canvas) return Promise.resolve(false);
|
||||||
@@ -202,10 +206,12 @@
|
|||||||
}
|
}
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
clickCanvasDownload(url, fileName);
|
clickCanvasDownload(url, fileName);
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}, 1e3);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}, "image/png");
|
}, "image/png");
|
||||||
} catch (_) {
|
} catch {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -213,7 +219,7 @@
|
|||||||
try {
|
try {
|
||||||
clickCanvasDownload(canvas.toDataURL("image/png"), fileName);
|
clickCanvasDownload(canvas.toDataURL("image/png"), fileName);
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
} catch (_) {
|
} catch {
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,7 +234,7 @@
|
|||||||
T.showHint(saved ? "Spectrum screenshot saved" : "Spectrum screenshot failed", saved ? 1500 : 1800);
|
T.showHint(saved ? "Spectrum screenshot saved" : "Spectrum screenshot failed", saved ? 1500 : 1800);
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
window.trx.modules.screenshot = {
|
screenshotWindow.trx.modules.screenshot = {
|
||||||
captureSpectrumScreenshot,
|
captureSpectrumScreenshot,
|
||||||
buildSpectrumSnapshotCanvas,
|
buildSpectrumSnapshotCanvas,
|
||||||
saveCanvasAsPng
|
saveCanvasAsPng
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ await build({
|
|||||||
app: path.join(sourceDir, "app.js"),
|
app: path.join(sourceDir, "app.js"),
|
||||||
"ui-core": path.join(sourceDir, "ui-core.ts"),
|
"ui-core": path.join(sourceDir, "ui-core.ts"),
|
||||||
"map-core": path.join(sourceDir, "map-core.js"),
|
"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"),
|
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
|
||||||
"leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.js"),
|
"leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.js"),
|
||||||
ais: path.join(sourceDir, "plugins", "ais.js"),
|
ais: path.join(sourceDir, "plugins", "ais.js"),
|
||||||
|
|||||||
+43
-23
@@ -4,11 +4,30 @@
|
|||||||
|
|
||||||
// Spectrum screenshot module (loaded on demand when user triggers screenshot).
|
// Spectrum screenshot module (loaded on demand when user triggers screenshot).
|
||||||
// Communicates with app.js core via window.trx namespace.
|
// 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<boolean>;
|
||||||
|
buildSpectrumSnapshotCanvas(): HTMLCanvasElement | null;
|
||||||
|
saveCanvasAsPng(canvas: HTMLCanvasElement | null, fileName: string): Promise<boolean>;
|
||||||
|
}
|
||||||
|
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 () {
|
(function () {
|
||||||
"use strict";
|
"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;
|
if (!el) return false;
|
||||||
const style = getComputedStyle(el);
|
const style = getComputedStyle(el);
|
||||||
if (style.display === "none" || style.visibility === "hidden") return false;
|
if (style.display === "none" || style.visibility === "hidden") return false;
|
||||||
@@ -18,7 +37,7 @@
|
|||||||
return rect.width > 0 && rect.height > 0;
|
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));
|
const radius = Math.max(0, Math.min(r, Math.min(w, h) / 2));
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(x + radius, y);
|
ctx.moveTo(x + radius, y);
|
||||||
@@ -33,7 +52,7 @@
|
|||||||
ctx.closePath();
|
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;
|
if (!isVisibleForSnapshot(el)) return null;
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const style = getComputedStyle(el);
|
const style = getComputedStyle(el);
|
||||||
@@ -49,26 +68,27 @@
|
|||||||
const bgAlpha = Math.min(bg[3], maxAlpha);
|
const bgAlpha = Math.min(bg[3], maxAlpha);
|
||||||
if (bgAlpha > 0.01) {
|
if (bgAlpha > 0.01) {
|
||||||
drawRoundedRectPath(ctx, x, y, w, h, radius);
|
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();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
const borderAlpha = Math.min(border[3], maxAlpha);
|
const borderAlpha = Math.min(border[3], maxAlpha);
|
||||||
if (borderWidth > 0 && borderAlpha > 0.01) {
|
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));
|
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.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();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
return { x, y, w, h, style };
|
return { x, y, w, h, style };
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight, maxLines) {
|
function drawWrappedText(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number, maxLines: number) {
|
||||||
const words = String(text || "").split(/\s+/).filter(Boolean);
|
const words = (text || "").split(/\s+/).filter(Boolean);
|
||||||
if (!words.length) return;
|
if (!words.length) return;
|
||||||
let line = "";
|
let line = "";
|
||||||
let lineIdx = 0;
|
let lineIdx = 0;
|
||||||
for (let i = 0; i < words.length; i += 1) {
|
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) {
|
if (ctx.measureText(candidate).width <= maxWidth || !line) {
|
||||||
line = candidate;
|
line = candidate;
|
||||||
continue;
|
continue;
|
||||||
@@ -76,14 +96,14 @@
|
|||||||
ctx.fillText(line, x, y + lineIdx * lineHeight);
|
ctx.fillText(line, x, y + lineIdx * lineHeight);
|
||||||
lineIdx += 1;
|
lineIdx += 1;
|
||||||
if (lineIdx >= maxLines) return;
|
if (lineIdx >= maxLines) return;
|
||||||
line = words[i];
|
line = word;
|
||||||
}
|
}
|
||||||
if (line && lineIdx < maxLines) {
|
if (line && lineIdx < maxLines) {
|
||||||
ctx.fillText(line, x, y + lineIdx * lineHeight);
|
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);
|
const chrome = drawElementChrome(ctx, el, rootRect, maxAlpha);
|
||||||
if (!chrome) return;
|
if (!chrome) return;
|
||||||
const text = (fallbackText == null ? el.innerText : fallbackText) || "";
|
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;
|
if (!isVisibleForSnapshot(axisEl)) return;
|
||||||
for (const node of axisEl.children) {
|
for (const node of axisEl.children) {
|
||||||
if (!(node instanceof HTMLElement)) continue;
|
if (!(node instanceof HTMLElement)) continue;
|
||||||
@@ -133,8 +153,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSpectrumSnapshotCanvas() {
|
function buildSpectrumSnapshotCanvas(): HTMLCanvasElement | null {
|
||||||
const rootEl = document.querySelector(".signal-visual-block");
|
const rootEl = document.querySelector<HTMLElement>(".signal-visual-block");
|
||||||
const spectrumPanelEl = document.getElementById("spectrum-panel");
|
const spectrumPanelEl = document.getElementById("spectrum-panel");
|
||||||
if (!rootEl || !isVisibleForSnapshot(rootEl) || !isVisibleForSnapshot(spectrumPanelEl)) {
|
if (!rootEl || !isVisibleForSnapshot(rootEl) || !isVisibleForSnapshot(spectrumPanelEl)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -145,7 +165,7 @@
|
|||||||
try {
|
try {
|
||||||
if (typeof gl.flush === "function") gl.flush();
|
if (typeof gl.flush === "function") gl.flush();
|
||||||
if (typeof gl.finish === "function") gl.finish();
|
if (typeof gl.finish === "function") gl.finish();
|
||||||
} catch (_) {
|
} catch {
|
||||||
// Ignore transient WebGL state errors and capture the last good frame.
|
// Ignore transient WebGL state errors and capture the last good frame.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +182,7 @@
|
|||||||
ctx.fillStyle = bg;
|
ctx.fillStyle = bg;
|
||||||
ctx.fillRect(0, 0, rootRect.width, rootRect.height);
|
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];
|
const canvases = [T.overviewCanvas, T.spectrumCanvas, signalOverlayCanvas];
|
||||||
for (const canvas of canvases) {
|
for (const canvas of canvases) {
|
||||||
if (!canvas || !isVisibleForSnapshot(canvas)) continue;
|
if (!canvas || !isVisibleForSnapshot(canvas)) continue;
|
||||||
@@ -205,7 +225,7 @@
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickCanvasDownload(href, fileName) {
|
function clickCanvasDownload(href: string, fileName: string) {
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = href;
|
a.href = href;
|
||||||
a.download = fileName;
|
a.download = fileName;
|
||||||
@@ -213,10 +233,10 @@
|
|||||||
a.style.display = "none";
|
a.style.display = "none";
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
requestAnimationFrame(() => a.remove());
|
requestAnimationFrame(() => { a.remove(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCanvasAsPng(canvas, fileName) {
|
function saveCanvasAsPng(canvas: HTMLCanvasElement | null, fileName: string): Promise<boolean> {
|
||||||
if (!canvas) return Promise.resolve(false);
|
if (!canvas) return Promise.resolve(false);
|
||||||
if (typeof canvas.toBlob === "function") {
|
if (typeof canvas.toBlob === "function") {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -228,10 +248,10 @@
|
|||||||
}
|
}
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
clickCanvasDownload(url, fileName);
|
clickCanvasDownload(url, fileName);
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
setTimeout(() => { URL.revokeObjectURL(url); }, 1000);
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}, "image/png");
|
}, "image/png");
|
||||||
} catch (_) {
|
} catch {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -239,7 +259,7 @@
|
|||||||
try {
|
try {
|
||||||
clickCanvasDownload(canvas.toDataURL("image/png"), fileName);
|
clickCanvasDownload(canvas.toDataURL("image/png"), fileName);
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
} catch (_) {
|
} catch {
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,7 +277,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register module API
|
// Register module API
|
||||||
window.trx.modules.screenshot = {
|
screenshotWindow.trx.modules.screenshot = {
|
||||||
captureSpectrumScreenshot,
|
captureSpectrumScreenshot,
|
||||||
buildSpectrumSnapshotCanvas,
|
buildSpectrumSnapshotCanvas,
|
||||||
saveCanvasAsPng,
|
saveCanvasAsPng,
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": false,
|
"checkJs": false,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"lib": ["ES2022", "DOM"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"types": []
|
"types": []
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.js"]
|
"include": ["src/**/*.ts", "src/**/*.js"]
|
||||||
|
|||||||
Reference in New Issue
Block a user