// views/panel.js — Zentrales Mehrmeter-Panel (wie in deinem Original) // Nutzt bis zu fünf HUD-Slots und zeichnet mittig ein breites Panel mit Rahmen. import { DEFAULT_TOP_INSET, FRAME_COLOR, FRAME_COLOR_DIM, LABEL_COLOR } from '../core/theme.js'; export const id = 'panel'; export function init() { return { staticLayer: null }; } export function resize() {} export function destroy(state) { state.staticLayer = null; } export async function render(env, state) { const { ctx: g, rect, config: CONFIG, meters } = env; const W = rect.w, H = rect.h; const mWidth = Math.max(200, Math.floor(W * 1)); const mPX = 0; const topInset = Number.isFinite(env?.topInset) ? Number(env.topInset) : DEFAULT_TOP_INSET; const mTop = Number.isFinite(env?.topInset) ? Math.max(0, Math.floor(topInset + 10)) : 80; const mBot = H - 10; const labelY = mTop - 25; // aktuell ungenutzt, Meter zeichnen eigene Labels // Slots nebeneinander (leere Slots werden ausgeblendet) const inner = 0; const gap = 12; const meterPadTop = 18; const meterPadBottom = 10; const slotsRaw = env.slots?.('panel') || ['vu','ppm-ebu','tp','rms','lufs']; const slots = slotsRaw.filter((v) => v && v !== 'none'); const n = slots.length; const setX0 = mPX + inner; if (!n) { g.save(); g.fillStyle = LABEL_COLOR; g.textAlign = 'left'; g.font = 'bold 16px ui-monospace, monospace'; g.fillText('Keine Slots aktiv', setX0 + 10, mTop + 20); g.restore(); return; } const setW = Math.floor((mWidth - (n - 1) * gap - inner * 2) / n); const staticLayout = { mPX, mTop, mWidth, H, gap, slots: n, setX0, setW, meterPadTop, meterPadBottom, dividersEnabled: !!CONFIG.PANEL_DIVIDERS_ENABLED, hasSlots: n > 0, }; for (let i = 0; i < n; i++) { const x = setX0 + i * (setW + gap); const rectM = { x, y: mTop + meterPadTop, w: setW, h: Math.max(0, (mBot - mTop) - meterPadTop - meterPadBottom) }; if (i > 0 && CONFIG.PANEL_DIVIDERS_ENABLED) { const dividerX = x - gap / 2; g.save(); g.strokeStyle = FRAME_COLOR_DIM; g.lineWidth = 1; g.setLineDash([4, 3]); g.beginPath(); g.moveTo(dividerX, mTop + meterPadTop - 6); g.lineTo(dividerX, mBot - meterPadBottom + 6); g.stroke(); g.restore(); } try { await meters.draw(g, rectM, slots[i], CONFIG); } catch (e) { // Slot neutral markieren, damit die anderen nicht leiden g.save(); g.strokeStyle = 'rgba(200,80,80,.8)'; g.setLineDash([6,4]); g.strokeRect(rectM.x+0.5, rectM.y+0.5, rectM.w-1, rectM.h-1); g.setLineDash([]); g.restore(); } } drawCachedPanelStaticLayer(g, state, staticLayout); } function drawCachedPanelStaticLayer(g, state, layout) { const { mPX, mTop, mWidth, H, gap, slots, setX0, setW, meterPadTop, meterPadBottom, dividersEnabled, } = layout; const key = [ Math.round(mPX), Math.round(mTop), Math.round(mWidth), Math.round(H), gap, slots, Math.round(setX0), Math.round(setW), meterPadTop, meterPadBottom, dividersEnabled ? 1 : 0, ].join('|'); const strokePad = 2; const width = Math.max(1, Math.round(mWidth + strokePad * 2)); const height = Math.max(1, Math.round((H - (mTop - 10)) + strokePad * 2)); const needsRebuild = !state.staticLayer || state.staticLayer.key !== key || state.staticLayer.width !== width || state.staticLayer.height !== height; if (needsRebuild) { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const cg = canvas.getContext('2d'); if (cg) { cg.save(); cg.translate(strokePad - mPX, strokePad - (mTop - 10)); cg.strokeStyle = FRAME_COLOR; cg.lineWidth = 2; cg.strokeRect(mPX, mTop - 10, mWidth, H - (mTop - 10)); if (dividersEnabled) { const mBot = H - 10; for (let i = 1; i < slots; i++) { const x = setX0 + i * (setW + gap); const dividerX = x - gap / 2; cg.save(); cg.strokeStyle = FRAME_COLOR_DIM; cg.lineWidth = 1; cg.setLineDash([4, 3]); cg.beginPath(); cg.moveTo(dividerX, mTop + meterPadTop - 6); cg.lineTo(dividerX, mBot - meterPadBottom + 6); cg.stroke(); cg.restore(); } } cg.restore(); } state.staticLayer = { key, canvas, width, height }; } g.drawImage(state.staticLayer.canvas, mPX - strokePad, (mTop - 10) - strokePad); }