diff --git a/src/main/main.ts b/src/main/main.ts index 4029181..55a0070 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -60,22 +60,10 @@ function createWindow() { const metadata = await window.overlayApi.metadata(); const settings = await window.overlayApi.loadSettings(); const radios = await window.overlayApi.discoverRadios(); - const csvPath = ${JSON.stringify(process.env.MT12_SMOKE_CSV || "")}; - let preview = null; - if (csvPath) { - const summary = await window.overlayApi.loadCsvSummary({ csv_path: csvPath, offset_ms: 0 }); - preview = await window.overlayApi.previewState({ - csv_path: csvPath, - time_ms: Math.min(1000, summary.duration_ms), - layout: settings.layout, - calibration: settings.calibration, - }); - } return { sourceCount: metadata.sources.length, layoutCount: Object.keys(settings.layout || {}).length, radioCount: radios.sources.length, - previewOk: preview ? Boolean(preview.state) : null, }; })(); `); diff --git a/src/main/nativeApi.ts b/src/main/nativeApi.ts index 3ab4c46..dd31dbb 100644 --- a/src/main/nativeApi.ts +++ b/src/main/nativeApi.ts @@ -3,19 +3,14 @@ import { execSync, spawn } from "node:child_process"; import { app } from "electron"; import { makeCanvas, renderFrameToCanvas, getRawFrame } from "./frameRenderer"; import { buildRunningStatsArray, getRunningStatsAt } from "../shared/widgetDraw"; +import { BAR_APPEARANCE_DEFAULTS as BAR_DEFAULTS, clamp, interpolateState } from "../shared/util"; +import type { CsvSample } from "../shared/types"; import https from "node:https"; import os from "node:os"; import path from "node:path"; -type Sample = { - time_ms: number; - values: Record; -}; - -type FrameState = Record; - type LoadedCsv = { - samples: Sample[]; + samples: CsvSample[]; sources: string[]; }; @@ -30,26 +25,16 @@ const TIME_WIDGET_TYPES = ["text"]; const LEGACY_VERTICAL_BAR_TO_BAR_SCALE_X = 330 / 220; const LEGACY_VERTICAL_BAR_TO_BAR_SCALE_Y = 130 / 48; const BAR_APPEARANCE_DEFAULTS = { - bar_track_fill_thickness: 68, - bar_track_outline_thickness: 3, - bar_center_mark_thickness: 2, - bar_corner_radius: 100, + bar_track_fill_thickness: BAR_DEFAULTS.trackFillThickness, + bar_track_outline_thickness: BAR_DEFAULTS.trackOutlineThickness, + bar_center_mark_thickness: BAR_DEFAULTS.centerMarkThickness, + bar_corner_radius: BAR_DEFAULTS.cornerRadius, }; -function clamp(value: number, low: number, high: number) { - if (!Number.isFinite(value)) return low; - return Math.max(low, Math.min(high, value)); -} - - function widgetTypesForSource(source: string) { return source === TIME_SOURCE ? TIME_WIDGET_TYPES : CHANNEL_WIDGET_TYPES; } -function sourceDisplayName(source: string) { - return source; -} - function defaultItemName(source: string, itemId: string) { const prefix = `item_${source}_`; if (itemId.startsWith(prefix)) { @@ -170,7 +155,7 @@ function defaultItemForSource(source: string, itemId: string) { const base = { source, name: defaultItemName(source, itemId), - label: sourceDisplayName(source), + label: source, widget: widgetTypesForSource(source)[0], x: 0.5, y: 0.5, @@ -377,7 +362,7 @@ function loadSamples(csvPath: string, offsetMs = 0): LoadedCsv { const index = Object.fromEntries(headers.map((header, idx) => [header, idx])); let firstTick: number | null = null; - const samples: Sample[] = []; + const samples: CsvSample[] = []; for (const line of lines.slice(1)) { const row = parseCsvLine(line); const tick = Number(row[index.timestamp]); @@ -397,25 +382,6 @@ function loadSamples(csvPath: string, offsetMs = 0): LoadedCsv { return { samples, sources }; } -function interpolateState(samples: Sample[], timeMs: number): FrameState { - if (timeMs <= samples[0].time_ms) return { ...samples[0].values }; - const last = samples[samples.length - 1]; - if (timeMs >= last.time_ms) return { ...last.values }; - let index = 0; - while (index < samples.length - 2 && samples[index + 1].time_ms < timeMs) index += 1; - const left = samples[index]; - const right = samples[index + 1]; - const segment = right.time_ms - left.time_ms; - const t = segment <= 0 ? 0 : (timeMs - left.time_ms) / segment; - const lerp = (a: number, b: number) => a + (b - a) * t; - const state: FrameState = {}; - const sources = new Set([...Object.keys(left.values), ...Object.keys(right.values)]); - for (const source of sources) { - state[source] = lerp(left.values[source] ?? 0, right.values[source] ?? 0); - } - return state; -} - function loadCsvSummary(payload: Record) { const csvPath = String(payload.csv_path || ""); const { samples, sources } = loadSamples(csvPath, Number(payload.offset_ms || 0)); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 64a53fa..5f651b9 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -23,16 +23,18 @@ import "./styles.css"; import "./i18n"; import { api, - clamp, defaultSettings, fallbackMetadata, - interpolateLocalState, +} from "./utils"; +import type { HandleId, ResizePreview, ResizingState } from "./utils"; +import { + clamp, + interpolateState, itemBounds, numeric, widgetSize, widgetTypesForSource, -} from "./utils"; -import type { HandleId, ResizePreview, ResizingState } from "./utils"; +} from "../shared/util"; import { buildRunningStatsArray, getRunningStatsAt, type RunningStats } from "../shared/widgetDraw"; import { CaptureRenderer } from "./components/CaptureRenderer"; import { LangDropdown } from "./components/LangDropdown"; @@ -219,7 +221,7 @@ function App() { async function refreshPreview(path = settings.csv_path, timeMs = previewTime, sourceSettings = settings) { if (!path) return; if (previewSamples.length) { - setPreviewState(interpolateLocalState(previewSamples, timeMs)); + setPreviewState(interpolateState(previewSamples, timeMs)); return; } const requestId = previewRequestRef.current + 1; @@ -251,7 +253,7 @@ function App() { void refreshPreview(settings.csv_path, timeMs, latestSettingsRef.current); return; } - setPreviewState(interpolateLocalState(samples, timeMs)); + setPreviewState(interpolateState(samples, timeMs)); } async function autoDetectFfmpeg() { diff --git a/src/renderer/components/SupportCard.tsx b/src/renderer/components/SupportCard.tsx new file mode 100644 index 0000000..adab4b6 --- /dev/null +++ b/src/renderer/components/SupportCard.tsx @@ -0,0 +1,53 @@ +import { useTranslation } from "react-i18next"; +import QRCode from "react-qr-code"; +import topercLogo from "../assets/toperc-logo.png"; + +export function SupportCard() { + const { t } = useTranslation(); + + return ( +
+
+ TopeRC +
+ TopeRC + {t("support.channelSub")} +
+ + {t("support.subscribe")} + +
+ +
+ +
+
+ +
+
+ {t("support.donateTitle")} + {t("support.donateSub")} + + paypal.me/dgarana + +
+
+
+ ); +} diff --git a/src/renderer/locales/de/translation.json b/src/renderer/locales/de/translation.json index d7b29a6..312baa1 100644 --- a/src/renderer/locales/de/translation.json +++ b/src/renderer/locales/de/translation.json @@ -67,9 +67,6 @@ "controlColor": "Farbe", "controlThickness": "Stärke", "controlThicknessPercent": "Stärke (%)", - "barTrackFillThickness": "Spur (%)", - "barTrackOutlineThickness": "Kontur", - "barCenterMarkThickness": "Mitte", "barCornerRadius": "Radius (%)", "selectOrAdd": "Widget auswählen oder hinzufügen", "zoomOut": "Herauszoomen", diff --git a/src/renderer/locales/en/translation.json b/src/renderer/locales/en/translation.json index d1873b0..28bc82e 100644 --- a/src/renderer/locales/en/translation.json +++ b/src/renderer/locales/en/translation.json @@ -67,9 +67,6 @@ "controlColor": "Color", "controlThickness": "Thickness", "controlThicknessPercent": "Thickness (%)", - "barTrackFillThickness": "Track (%)", - "barTrackOutlineThickness": "Outline", - "barCenterMarkThickness": "Center mark", "barCornerRadius": "Radius (%)", "selectOrAdd": "Select or add a widget", "zoomOut": "Zoom out", diff --git a/src/renderer/locales/es/translation.json b/src/renderer/locales/es/translation.json index f4827ce..fdd32d2 100644 --- a/src/renderer/locales/es/translation.json +++ b/src/renderer/locales/es/translation.json @@ -67,9 +67,6 @@ "controlColor": "Color", "controlThickness": "Grosor", "controlThicknessPercent": "Grosor (%)", - "barTrackFillThickness": "Pista (%)", - "barTrackOutlineThickness": "Contorno", - "barCenterMarkThickness": "Marca central", "barCornerRadius": "Redondeado (%)", "selectOrAdd": "Selecciona o añade un widget", "zoomOut": "Alejar", diff --git a/src/renderer/locales/fr/translation.json b/src/renderer/locales/fr/translation.json index 16b5742..83732a2 100644 --- a/src/renderer/locales/fr/translation.json +++ b/src/renderer/locales/fr/translation.json @@ -67,9 +67,6 @@ "controlColor": "Couleur", "controlThickness": "Epaisseur", "controlThicknessPercent": "Epaisseur (%)", - "barTrackFillThickness": "Piste (%)", - "barTrackOutlineThickness": "Contour", - "barCenterMarkThickness": "Repère central", "barCornerRadius": "Arrondi (%)", "selectOrAdd": "Sélectionner ou ajouter un widget", "zoomOut": "Dézoomer", diff --git a/src/renderer/utils.ts b/src/renderer/utils.ts index dee7f10..127460e 100644 --- a/src/renderer/utils.ts +++ b/src/renderer/utils.ts @@ -1,29 +1,9 @@ -import type { - AppMetadata, - AppSettings, - CsvSample, - FrameState, - LayoutItem, - OverlayApi, -} from "../shared/types"; +import type { AppMetadata, AppSettings, LayoutItem } from "../shared/types"; export type HandleId = "nw" | "n" | "ne" | "e" | "se" | "s" | "sw" | "w"; -export const HANDLE_CURSORS: Record = { - nw: "nw-resize", n: "n-resize", ne: "ne-resize", - w: "w-resize", e: "e-resize", - sw: "sw-resize", s: "s-resize", se: "se-resize", -}; - export type ResizePreview = { itemId: string; x: number; y: number; scaleX: number; scaleY: number }; -export const BAR_APPEARANCE_DEFAULTS = { - trackFillThickness: 68, - trackOutlineThickness: 3, - centerMarkThickness: 2, - cornerRadius: 100, -}; - export type ResizingState = { itemId: string; handle: HandleId; @@ -55,184 +35,12 @@ export const fallbackMetadata: AppMetadata = { time_widget_types: ["text"], }; -const fallbackBarAppearance = { - bar_track_fill_thickness: BAR_APPEARANCE_DEFAULTS.trackFillThickness, - bar_track_outline_thickness: BAR_APPEARANCE_DEFAULTS.trackOutlineThickness, - bar_center_mark_thickness: BAR_APPEARANCE_DEFAULTS.centerMarkThickness, - bar_corner_radius: BAR_APPEARANCE_DEFAULTS.cornerRadius, -}; - -export const fallbackLayout: Record = { - item_time_1: { - source: "time", - name: "Timer", - label: "TIME", - widget: "text", - x: 0.13, - y: 0.07, - scale_x: 1.15, - scale_y: 1.08, - rotation: 0, - accent_color: "#55beff", - negative_color: "#55beff", - positive_color: "#55beff", - text_color: "#ffffff", - bg_color: "#141a20", - bg_visible: true, - outline_color: "#ffffff", - outline_visible: true, - shadow_visible: true, - }, - item_ch1_1: { - source: "ch1", - name: "Steering", - label: "STEER", - widget: "gauge", - x: 0.16, - y: 0.76, - scale_x: 1.15, - scale_y: 1.15, - rotation: 0, - accent_color: "#ffd25a", - negative_color: "#ffaa54", - positive_color: "#55beff", - text_color: "#ffffff", - bg_color: "#141a20", - bg_visible: true, - outline_color: "#ffffff", - outline_visible: true, - shadow_visible: true, - }, - item_ch2_1: { - source: "ch2", - name: "Throttle / brake", - label: "THROTTLE", - widget: "bar", - x: 0.86, - y: 0.72, - scale_x: 1.75, - scale_y: 2.35, - rotation: -90, - accent_color: "#40d68c", - negative_color: "#ff5c5c", - positive_color: "#40d68c", - text_color: "#ffffff", - bg_color: "#141a20", - bg_visible: true, - outline_color: "#ffffff", - outline_visible: true, - shadow_visible: true, - ...fallbackBarAppearance, - }, - item_ch3_1: { - source: "ch3", - name: "Aux 1", - label: "AUX 1", - widget: "bar", - x: 0.74, - y: 0.08, - scale_x: 1.65, - scale_y: 1, - rotation: 0, - accent_color: "#55beff", - negative_color: "#ffaa54", - positive_color: "#55beff", - text_color: "#ffffff", - bg_color: "#141a20", - bg_visible: true, - outline_color: "#ffffff", - outline_visible: true, - shadow_visible: true, - ...fallbackBarAppearance, - }, - item_ch4_1: { - source: "ch4", - name: "Aux 2", - label: "AUX 2", - widget: "bar", - x: 0.74, - y: 0.15, - scale_x: 1.65, - scale_y: 1, - rotation: 0, - accent_color: "#ffaa54", - negative_color: "#ffaa54", - positive_color: "#55beff", - text_color: "#ffffff", - bg_color: "#141a20", - bg_visible: true, - outline_color: "#ffffff", - outline_visible: true, - shadow_visible: true, - ...fallbackBarAppearance, - }, -}; - -export const fallbackItem: LayoutItem = fallbackLayout.item_ch1_1; - -function fallbackLayoutClone() { - return Object.fromEntries( - Object.entries(fallbackLayout).map(([id, item]) => [id, { ...item }]), - ) as Record; -} - -export const browserFallbackApi: OverlayApi = { - metadata: async () => fallbackMetadata, - defaultLayout: async () => ({ layout: fallbackLayoutClone() }), - loadSettings: async () => ({ ...defaultSettings, layout: fallbackLayoutClone() }), - saveSettings: async (settings) => settings, - chooseCsv: async () => null, - chooseDirectory: async () => null, - chooseMovOutput: async () => null, - chooseFfmpeg: async () => null, - loadCsvSummary: async () => ({ - csv_path: "", - sample_count: 0, - duration_ms: 0, - scale_mode: "preview", - sources: fallbackMetadata.sources, - }), - previewState: async () => ({ time_ms: 0, state: {} }), - renderOverlay: async () => ({ frame_count: 0, output_dir: "", video_output: "" }), - discoverRadios: async () => ({ sources: [] }), - listRadioLogs: async () => ({ logs: [] }), - createWidget: async () => ({ item_id: `item_ch1_${Date.now()}`, item: { ...fallbackItem } }), - discoverFfmpeg: async () => ({ path: null, source: "not found" }), - downloadFfmpeg: async () => { throw new Error("Not available in browser"); }, - installScripts: async () => { throw new Error("Not available in browser"); }, - onBridgeEvent: () => () => undefined, -}; - -export const api = window.overlayApi ?? browserFallbackApi; - -export function numeric(value: number | string | undefined, fallback: number) { - if (value === undefined || value === "") return fallback; - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : fallback; -} - -export function clamp(value: number, low: number, high: number) { - if (!Number.isFinite(value)) return low; - return Math.max(low, Math.min(high, value)); -} - -export function widgetSize(widget: string) { - const sizes: Record = { - text: [280, 52], - bar: [220, 48], - gauge: [250, 250], - }; - return sizes[widget] || [180, 60]; -} +export const api = window.overlayApi; export function itemName(id: string, item: LayoutItem) { return item.name || item.label || id; } -export function widgetTypesForSource(metadata: AppMetadata, source: string) { - return source === "time" ? metadata.time_widget_types : metadata.channel_widget_types; -} - export function widgetTypeLabel(widget: string) { return widget.replace(/_/g, " "); } @@ -278,35 +86,3 @@ export function colorControlLabel(item: LayoutItem, key: ColorKey): string | nul return key; } - -export function interpolateLocalState(samples: CsvSample[], timeMs: number): FrameState { - if (!samples.length) return {}; - if (timeMs <= samples[0].time_ms) return { ...samples[0].values }; - const last = samples[samples.length - 1]; - if (timeMs >= last.time_ms) return { ...last.values }; - let index = 0; - while (index < samples.length - 2 && samples[index + 1].time_ms < timeMs) index += 1; - const left = samples[index]; - const right = samples[index + 1]; - const segment = right.time_ms - left.time_ms; - const t = segment <= 0 ? 0 : (timeMs - left.time_ms) / segment; - const lerp = (a: number, b: number) => a + (b - a) * t; - const state: FrameState = {}; - const sources = new Set([...Object.keys(left.values), ...Object.keys(right.values)]); - for (const source of sources) { - state[source] = lerp(left.values[source] ?? 0, right.values[source] ?? 0); - } - return state; -} - -export function itemBounds(item: LayoutItem, frameWidth: number, frameHeight: number): [number, number, number, number] { - const [baseWidth, baseHeight] = widgetSize(item.widget); - const scale = Math.max(0.2, Math.min(frameWidth / 1920, frameHeight / 1080)); - const width = Math.max(32, baseWidth * scale * Number(item.scale_x || 1)); - const height = Math.max(24, baseHeight * scale * Number(item.scale_y || 1)); - const centerX = item.x * frameWidth; - const centerY = item.y * frameHeight; - const left = clamp(centerX - width / 2, 0, Math.max(0, frameWidth - width)); - const top = clamp(centerY - height / 2, 0, Math.max(0, frameHeight - height)); - return [left, top, left + width, top + height]; -} diff --git a/src/renderer/views/InstallView.tsx b/src/renderer/views/InstallView.tsx index c282dd7..62ecc2a 100644 --- a/src/renderer/views/InstallView.tsx +++ b/src/renderer/views/InstallView.tsx @@ -1,6 +1,4 @@ import { useTranslation } from "react-i18next"; -import QRCode from "react-qr-code"; -import topercLogo from "../assets/toperc-logo.png"; import { Antenna, Download, @@ -8,6 +6,7 @@ import { RefreshCcw, } from "lucide-react"; import type { RadioSource } from "../../shared/types"; +import { SupportCard } from "../components/SupportCard"; export interface InstallViewProps { busy: boolean; @@ -118,49 +117,7 @@ export function InstallView(props: InstallViewProps) {
)} -
-
- TopeRC -
- TopeRC - {t("support.channelSub")} -
- - {t("support.subscribe")} - -
- -
- -
-
- -
-
- {t("support.donateTitle")} - {t("support.donateSub")} - - paypal.me/dgarana - -
-
-
+
); } diff --git a/src/renderer/views/LayoutView.tsx b/src/renderer/views/LayoutView.tsx index 8afcaee..be1f12f 100644 --- a/src/renderer/views/LayoutView.tsx +++ b/src/renderer/views/LayoutView.tsx @@ -5,15 +5,17 @@ import type { AppMetadata, AppSettings, CsvSummary, FrameState, LayoutItem } fro import type { RunningStats } from "../../shared/widgetDraw"; import { WidgetCanvas } from "../components/WidgetCanvas"; import { - BAR_APPEARANCE_DEFAULTS, - clamp, colorControlLabel, itemName, - widgetSize, - widgetTypesForSource, widgetTypeLabel, } from "../utils"; import type { ColorKey, HandleId } from "../utils"; +import { + BAR_APPEARANCE_DEFAULTS, + clamp, + widgetSize, + widgetTypesForSource, +} from "../../shared/util"; export interface LayoutViewProps { settings: AppSettings; diff --git a/src/renderer/views/SourceView.tsx b/src/renderer/views/SourceView.tsx index b8a1105..81b2484 100644 --- a/src/renderer/views/SourceView.tsx +++ b/src/renderer/views/SourceView.tsx @@ -1,6 +1,4 @@ import { useTranslation } from "react-i18next"; -import QRCode from "react-qr-code"; -import topercLogo from "../assets/toperc-logo.png"; import { Antenna, ArrowRight, @@ -8,6 +6,7 @@ import { RefreshCcw, } from "lucide-react"; import type { AppSettings, CsvSummary, RadioLog, RadioSource } from "../../shared/types"; +import { SupportCard } from "../components/SupportCard"; export interface SourceViewProps { settings: AppSettings; @@ -149,49 +148,7 @@ export function SourceView(props: SourceViewProps) { )} -
-
- TopeRC -
- TopeRC - {t("support.channelSub")} -
- - {t("support.subscribe")} - -
- -
- -
-
- -
-
- {t("support.donateTitle")} - {t("support.donateSub")} - - paypal.me/dgarana - -
-
-
+
); } diff --git a/src/shared/util.ts b/src/shared/util.ts new file mode 100644 index 0000000..45beae3 --- /dev/null +++ b/src/shared/util.ts @@ -0,0 +1,68 @@ +import type { AppMetadata, CsvSample, FrameState, LayoutItem } from "./types"; + +export function clamp(value: number, low: number, high: number): number { + if (!Number.isFinite(value)) return low; + return Math.max(low, Math.min(high, value)); +} + +export function numeric(value: number | string | undefined, fallback: number): number { + if (value === undefined || value === "") return fallback; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +} + +export const BAR_APPEARANCE_DEFAULTS = { + trackFillThickness: 68, + trackOutlineThickness: 3, + centerMarkThickness: 2, + cornerRadius: 100, +}; + +export function widgetSize(widget: string): [number, number] { + const sizes: Record = { + text: [280, 52], + bar: [220, 48], + gauge: [250, 250], + }; + return sizes[widget] ?? [180, 60]; +} + +export function widgetTypesForSource(metadata: AppMetadata, source: string): string[] { + return source === "time" ? metadata.time_widget_types : metadata.channel_widget_types; +} + +export function itemBounds( + item: LayoutItem, + frameWidth: number, + frameHeight: number, +): [number, number, number, number] { + const [baseWidth, baseHeight] = widgetSize(item.widget); + const scale = Math.max(0.2, Math.min(frameWidth / 1920, frameHeight / 1080)); + const width = Math.max(32, baseWidth * scale * Number(item.scale_x || 1)); + const height = Math.max(24, baseHeight * scale * Number(item.scale_y || 1)); + const centerX = item.x * frameWidth; + const centerY = item.y * frameHeight; + const left = clamp(centerX - width / 2, 0, Math.max(0, frameWidth - width)); + const top = clamp(centerY - height / 2, 0, Math.max(0, frameHeight - height)); + return [left, top, left + width, top + height]; +} + +export function interpolateState(samples: CsvSample[], timeMs: number): FrameState { + if (!samples.length) return {}; + if (timeMs <= samples[0].time_ms) return { ...samples[0].values }; + const last = samples[samples.length - 1]; + if (timeMs >= last.time_ms) return { ...last.values }; + let index = 0; + while (index < samples.length - 2 && samples[index + 1].time_ms < timeMs) index += 1; + const left = samples[index]; + const right = samples[index + 1]; + const segment = right.time_ms - left.time_ms; + const t = segment <= 0 ? 0 : (timeMs - left.time_ms) / segment; + const lerp = (a: number, b: number) => a + (b - a) * t; + const state: FrameState = {}; + const sources = new Set([...Object.keys(left.values), ...Object.keys(right.values)]); + for (const source of sources) { + state[source] = lerp(left.values[source] ?? 0, right.values[source] ?? 0); + } + return state; +} diff --git a/src/shared/widgetDraw.ts b/src/shared/widgetDraw.ts index 76c39dd..405fcab 100644 --- a/src/shared/widgetDraw.ts +++ b/src/shared/widgetDraw.ts @@ -6,6 +6,7 @@ */ import type { CsvSample, LayoutItem } from "./types"; +import { BAR_APPEARANCE_DEFAULTS, clamp, itemBounds } from "./util"; export type FrameState = Record; @@ -95,10 +96,6 @@ function applyTransforms(value: number, transforms: string[] | undefined, stats: return v; } -function clamp(v: number, lo: number, hi: number): number { - return Math.max(lo, Math.min(hi, v)); -} - /** Parse '#rrggbb' or '#rgb' → 'rgba(r,g,b,a)' */ function rgba(hex: string, alpha: number): string { const h = hex.replace("#", ""); @@ -111,10 +108,6 @@ function rgba(hex: string, alpha: number): string { /** CSS: background-color: ${bg_color}aa → 0xAA/0xFF ≈ 0.6667 */ const BG_ALPHA = 170 / 255; -const BAR_TRACK_FILL_THICKNESS = 68; -const BAR_TRACK_OUTLINE_THICKNESS = 3; -const BAR_CENTER_MARK_THICKNESS = 2; -const BAR_CORNER_RADIUS = 100; function optionalNumber(value: number | undefined, fallback: number, low: number, high: number): number { const parsed = Number(value ?? fallback); @@ -126,31 +119,6 @@ function formatValue(value: number): string { return `${v >= 0 ? "+" : ""}${v}`; } -function widgetBaseSize(widget: string): [number, number] { - const sizes: Record = { - text: [280, 52], - bar: [220, 48], - gauge: [250, 250], - }; - return sizes[widget] ?? [180, 60]; -} - -function itemBoundsFromLayout( - item: LayoutItem, - fw: number, - fh: number, -): [number, number, number, number] { - const [bw, bh] = widgetBaseSize(item.widget); - const s = clamp(Math.min(fw / 1920, fh / 1080), 0.1, 8); - const w = Math.max(32, bw * s * (item.scale_x || 1)); - const h = Math.max(24, bh * s * (item.scale_y || 1)); - const cx = item.x * fw; - const cy = item.y * fh; - const left = clamp(cx - w / 2, 0, Math.max(0, fw - w)); - const top = clamp(cy - h / 2, 0, Math.max(0, fh - h)); - return [left, top, left + w, top + h]; -} - function valueForSource(state: FrameState, source: string): number { const value = Number(state[source]); return Number.isFinite(value) ? value : 0; @@ -247,15 +215,15 @@ function drawGauge(ctx: DrawCtx, item: LayoutItem, value: number, w: number, h: function drawBar(ctx: DrawCtx, item: LayoutItem, value: number, w: number, h: number, sc: number) { const trackW = 0.90 * w; - const trackFillPct = optionalNumber(item.bar_track_fill_thickness, BAR_TRACK_FILL_THICKNESS, 5, 100) / 100; + const trackFillPct = optionalNumber(item.bar_track_fill_thickness, BAR_APPEARANCE_DEFAULTS.trackFillThickness, 5, 100) / 100; const trackH = Math.max(1, trackFillPct * h); const trackX = (w - trackW) / 2; const trackY = (h - trackH) / 2; const outlineW = item.outline_visible !== false - ? Math.min(optionalNumber(item.bar_track_outline_thickness, BAR_TRACK_OUTLINE_THICKNESS, 0, 24) * sc, trackW / 2, trackH / 2) + ? Math.min(optionalNumber(item.bar_track_outline_thickness, BAR_APPEARANCE_DEFAULTS.trackOutlineThickness, 0, 24) * sc, trackW / 2, trackH / 2) : 0; const innerInset = outlineW; - const cornerPct = optionalNumber(item.bar_corner_radius, BAR_CORNER_RADIUS, 0, 100) / 100; + const cornerPct = optionalNumber(item.bar_corner_radius, BAR_APPEARANCE_DEFAULTS.cornerRadius, 0, 100) / 100; const outerRadius = (trackH / 2) * cornerPct; if (item.shadow_visible !== false) { @@ -301,7 +269,7 @@ function drawBar(ctx: DrawCtx, item: LayoutItem, value: number, w: number, h: nu } // Center tick line (geometric guide, always visible) - const centerMarkW = optionalNumber(item.bar_center_mark_thickness, BAR_CENTER_MARK_THICKNESS, 0, 24) * sc; + const centerMarkW = optionalNumber(item.bar_center_mark_thickness, BAR_APPEARANCE_DEFAULTS.centerMarkThickness, 0, 24) * sc; if (centerMarkW > 0 && fillH > 0.5) { ctx.fillStyle = item.text_color; ctx.fillRect(midX - centerMarkW / 2, fillY, centerMarkW, fillH); @@ -346,7 +314,7 @@ function drawWidget( fw: number, fh: number, ) { - const [left, top, right, bottom] = itemBoundsFromLayout(item, fw, fh); + const [left, top, right, bottom] = itemBounds(item, fw, fh); const w = right - left; const h = bottom - top; const sc = clamp(Math.min(fw / 1920, fh / 1080), 0.1, 8);