Initial Phoenix analyzer

This commit is contained in:
Mikei386
2026-06-02 20:56:19 +02:00
commit e499bac928
62 changed files with 28893 additions and 0 deletions
+449
View File
@@ -0,0 +1,449 @@
// views/clock.js — Vollbild-Studiosclock mit "Zurück"-Button
const DOT_FONT = {
'0': ['01110','10001','10011','10101','11001','10001','01110'],
'1': ['00100','01100','00100','00100','00100','00100','01110'],
'2': ['01110','10001','00001','00110','01000','10000','11111'],
'3': ['11110','00001','00001','01110','00001','00001','11110'],
'4': ['00010','00110','01010','10010','11111','00010','00010'],
'5': ['11111','10000','11110','00001','00001','10001','01110'],
'6': ['00110','01000','10000','11110','10001','10001','01110'],
'7': ['11111','00001','00010','00100','01000','01000','01000'],
'8': ['01110','10001','10001','01110','10001','10001','01110'],
'9': ['01110','10001','10001','01111','00001','00010','01100'],
':': ['0','1','0','0','1','0','0'],
'.': ['000','000','000','000','000','000','010'],
};
const DOT_COLS = DOT_FONT['0'][0].length;
const DIN_SCALE = [
{ db: -50, pos: 0.0205 },
{ db: -40, pos: 0.0564 },
{ db: -35, pos: 0.0974 },
{ db: -30, pos: 0.1538 },
{ db: -25, pos: 0.2308 },
{ db: -20, pos: 0.3179 },
{ db: -15, pos: 0.4359 },
{ db: -10, pos: 0.5538 },
{ db: -5, pos: 0.7026 },
{ db: 0, pos: 0.8513 },
{ db: +5, pos: 1.0000 },
];
export const id = 'clock';
export function init(env) {
const btn = document.createElement('button');
btn.textContent = 'Vollbild';
btn.className = 'btn btn-compact';
Object.assign(btn.style, {
position: 'fixed',
top: '10px',
right: '10px',
zIndex: 12000,
padding: '4px 8px',
cursor: 'pointer',
});
btn.onclick = () => {
toggleFullscreen(env, btn);
};
document.body.appendChild(btn);
return { btn, fullscreen: false };
}
export function destroy(state) {
if (state?.btn && state.btn.parentNode) state.btn.parentNode.removeChild(state.btn);
}
export function resize() {}
export async function render(env, state) {
const { ctx: g, rect } = env;
const styleCfg = env.config?.CLOCK_STYLE;
if (styleCfg === 'digital') {
drawClockDigital(g, rect, env, { showPpm: false });
} else if (styleCfg === 'digital-ppm') {
drawClockDigital(g, rect, env, { showPpm: true });
} else if (styleCfg === 'analog-ppm') {
drawClockAnalog(g, rect, env, { showPpm: true });
} else {
drawClockAnalog(g, rect, env, { showPpm: false });
}
}
function toggleFullscreen(env, btn) {
const hud = document.querySelector('.hud');
const options = document.getElementById('optionsPanelNew');
const isFs = btn.dataset.fs === '1';
if (isFs) {
if (hud) hud.style.display = '';
if (options) options.style.display = (env.style === 'options-panel') ? 'block' : 'none';
btn.textContent = 'Vollbild';
btn.dataset.fs = '0';
} else {
if (hud) hud.style.display = 'none';
if (options) options.style.display = 'none';
btn.textContent = '↩';
btn.dataset.fs = '1';
}
}
function drawClockAnalog(ctx, rect, env, opts = {}) {
const showPpm = !!opts.showPpm;
const d = new Date();
const hours = d.getHours();
const mins = d.getMinutes();
const secs = d.getSeconds() + d.getMilliseconds() / 1000;
const centerX = rect.x + rect.w / 2;
const centerY = rect.y + rect.h / 2;
const radius = Math.min(rect.w, rect.h) * 0.48;
const baseDot = Math.max(1.5, Math.round(radius * 0.03));
const ringDot = baseDot * 0.7;
const glyphDot = baseDot * 1.3;
const glow = env.config?.CLOCK_LED_GLOW !== false;
const color = normalizeColor(env.config?.CLOCK_LED_COLOR || '#ff0000');
ctx.save();
ctx.fillStyle = 'rgba(8,8,12,1)';
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
ctx.translate(centerX, centerY);
for (let i = 0; i < 60; i++) {
const ang = (Math.PI * 2 * i) / 60 - Math.PI / 2;
const r = radius * 0.88;
const filled = i <= secs;
const cx = Math.cos(ang) * r;
const cy = Math.sin(ang) * r;
if (filled) {
drawLed(ctx, cx, cy, ringDot, glow, color);
} else {
ctx.fillStyle = 'rgba(120,120,120,0.25)';
ctx.beginPath();
ctx.arc(cx, cy, ringDot, 0, Math.PI * 2);
ctx.fill();
}
if (i % 5 === 0) {
const outerR = r + ringDot * 3.2;
drawLed(ctx, Math.cos(ang) * outerR, Math.sin(ang) * outerR, ringDot, glow, color);
}
}
const timeStr = `${pad2(hours)}:${pad2(mins)}`;
const glyphSize = radius * 0.32;
const timeSpacing = 0.9; // wie Screensaver
const timeY = -(glyphSize * 1.2) / 2; // vertikal zentriert
const blinkOn = isSecondPulseActive(d);
const extraGaps = [0, 3]; // wie Screensaver (Luft zwischen Blöcken)
const metrics = measureTextLayout(timeStr, glyphSize, timeSpacing, extraGaps);
const timeX = -(metrics.colonCenter ?? metrics.width / 2); // Doppelpunkt auf Mitte
drawDotText(ctx, timeStr, timeX, timeY, glyphSize, glyphDot * 1.1, timeSpacing, blinkOn ? (glyphDot * 1.1) : 0, extraGaps, glow, color);
if (showPpm) {
const ppmWidth = radius * 1.2;
const ppmY = glyphSize * 1.0; // etwas mehr Abstand zur Uhrzeit
const ppmLevels = {
L: mapPpmRawToDin(Number.isFinite(env.audio?.ppmDinL) ? env.audio.ppmDinL : env.audio?.ppmL, env.config),
R: mapPpmRawToDin(Number.isFinite(env.audio?.ppmDinR) ? env.audio.ppmDinR : env.audio?.ppmR, env.config),
};
drawMiniPpm(ctx, { cx: 0, cy: ppmY }, ppmWidth, glyphDot, env, ppmLevels);
}
ctx.restore();
}
function drawClockDigital(ctx, rect, env, opts = {}) {
const showPpm = !!opts.showPpm;
const d = new Date();
const hours = d.getHours();
const mins = d.getMinutes();
const secs = d.getSeconds() + d.getMilliseconds() / 1000;
const day = d.getDate();
const month = d.getMonth() + 1;
const year = d.getFullYear();
const centerX = rect.x + rect.w / 2;
const centerY = rect.y + rect.h / 2;
const radius = Math.min(rect.w, rect.h) * 0.48;
const baseDot = Math.max(1.5, Math.round(radius * 0.028));
const glyphDot = baseDot * 1.3;
const glyphSize = radius * 0.32;
const timeSpacing = 1.2;
const blinkOn = isSecondPulseActive(d);
const extraGaps = [0, 3, 6]; // nach HH, MM, SS
const color = normalizeColor(env.config?.CLOCK_LED_COLOR || '#ff0000');
const glow = env.config?.CLOCK_LED_GLOW !== false;
ctx.save();
ctx.fillStyle = 'rgba(8,8,12,1)';
ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
ctx.translate(centerX, centerY);
const timeStr = `${pad2(hours)}:${pad2(mins)}:${pad2(Math.floor(secs))}`;
const dateStr = `${pad2(day)}.${pad2(month)}.${year}`;
const dateSize = glyphSize * 0.7;
const dateSpacing = 1.1;
const dateGaps = [1, 4];
const timeMetrics = measureTextLayout(timeStr, glyphSize, timeSpacing, extraGaps);
const dateMetrics = measureTextLayout(dateStr, dateSize, dateSpacing, dateGaps);
const lineGap = glyphSize * 0.6;
const totalH = glyphSize * 1.2 + dateSize * 1.2 + lineGap;
const startY = -totalH / 2;
const timeX = -timeMetrics.width / 2;
const timeY = startY;
drawDotText(ctx, timeStr, timeX, timeY, glyphSize, glyphDot, timeSpacing, blinkOn ? glyphDot : 0, extraGaps, glow, color);
const dateX = -dateMetrics.width / 2;
const dateY = startY + glyphSize * 1.2 + lineGap;
drawDotText(ctx, dateStr, dateX, dateY, dateSize, glyphDot * 0.9, dateSpacing, null, dateGaps, glow, color, dateSize * 0.8);
if (showPpm) {
const ppmLevels = {
L: mapPpmRawToDin(Number.isFinite(env.audio?.ppmDinL) ? env.audio.ppmDinL : env.audio?.ppmL, env.config),
R: mapPpmRawToDin(Number.isFinite(env.audio?.ppmDinR) ? env.audio.ppmDinR : env.audio?.ppmR, env.config),
};
const ppmWidth = dateMetrics.width;
const dateBottom = dateY + dateSize * 1.2;
const bottom = rect.h / 2;
const ppmY = dateBottom + (bottom - dateBottom) * 0.5;
drawMiniPpm(ctx, { cx: 0, cy: ppmY }, ppmWidth, glyphDot, env, ppmLevels);
}
ctx.restore();
}
function drawMiniPpm(ctx, pos, width, dotSize, env, levels) {
const normalCol = normalizeColor(env.config?.CLOCK_LED_COLOR || '#ff0000');
const warnCol = normalizeColor(env.config?.CLOCK_LED_COLOR || '#ff0000');
const bgCol = 'rgba(255,255,255,0.08)';
// Feste Farbe wie die %Beschriftung im PPM-DIN-Meter, etwas dunkler
const tickCol = '#ff9922';
const minDb = DIN_SCALE[0].db;
const maxDb = DIN_SCALE[DIN_SCALE.length - 1].db;
const lvl = [
Number.isFinite(levels?.L) ? levels.L : mapPpmRawToDin(Number.isFinite(env.audio?.ppmDinL) ? env.audio.ppmDinL : env.audio?.ppmL, env.config),
Number.isFinite(levels?.R) ? levels.R : mapPpmRawToDin(Number.isFinite(env.audio?.ppmDinR) ? env.audio.ppmDinR : env.audio?.ppmR, env.config),
];
const h = Math.max(4, dotSize * 1.4);
const gap = h * 0.6;
const halfW = width / 2;
const ticks = [-50, -40, -30, -20, -10, -5, 0, +5];
const norm = (db) => {
const clamped = Math.min(maxDb, Math.max(minDb, db));
for (let i = 0; i < DIN_SCALE.length - 1; i++) {
const a = DIN_SCALE[i];
const b = DIN_SCALE[i + 1];
if (clamped >= a.db && clamped <= b.db) {
const t = (clamped - a.db) / (b.db - a.db || 1);
return a.pos + t * (b.pos - a.pos);
}
}
return clamped >= maxDb ? DIN_SCALE[DIN_SCALE.length - 1].pos : DIN_SCALE[0].pos;
};
ctx.save();
ctx.translate(pos.cx, pos.cy);
for (let ch = 0; ch < 2; ch++) {
const y = (ch === 0 ? - (h + gap) * 0.5 : (h + gap) * 0.5);
ctx.fillStyle = bgCol;
ctx.fillRect(-halfW, y - h / 2, width, h);
const val = norm(lvl[ch]);
const fillW = width * val;
const over = lvl[ch] > 0;
const colNorm = normalCol;
const colWarn = normalizeColor('#ff5050');
const norm0 = norm(0);
const warnStartX = -halfW + norm0 * width;
const ledMode = env.config?.CLOCK_PPM_LED_MODE === true;
const drawTicksAndLabels = () => {
ctx.fillStyle = tickCol;
ctx.font = `${Math.max(5, h * 0.8)}px ui-monospace, monospace`;
ticks.forEach((t) => {
const x = -halfW + norm(t) * width;
const tickLen = (ch === 1) ? h * 1.8 : h * 1.2; // unterer Balken: längere Ticks
ctx.fillRect(x - 0.5, y - h * 0.6, 1, tickLen);
if (ch === 1) {
ctx.textAlign = (t === -50 ? 'right' : 'left');
ctx.textBaseline = 'bottom';
const labelX = t === -50 ? (x - h * 0.2) : (x + h * 0.2);
ctx.fillText(String(t), labelX, y + tickLen + h * 0.15);
}
});
};
if (ledMode) {
// Nur untere Label (ch==1) zeichnen, keine Tick-Striche über den LEDs
if (ch === 1) {
ticks.forEach((t) => {
const x = -halfW + norm(t) * width;
const tickLen = h * 1.8;
const labelY = y + tickLen + h * 0.15;
// Verbindungsstrich nur unterhalb des Meters (bis zum Meter-Anfang)
const lineTop = y + h * 0.5;
ctx.fillStyle = tickCol;
ctx.fillRect(x - 0.5, lineTop, 1, Math.max(0, labelY - lineTop));
ctx.fillStyle = tickCol;
ctx.font = `${Math.max(5, h * 0.8)}px ui-monospace, monospace`;
ctx.textAlign = (t === -50 ? 'right' : 'left');
ctx.textBaseline = 'bottom';
const labelX = t === -50 ? (x - h * 0.2) : (x + h * 0.2);
ctx.fillText(String(t), labelX, labelY);
});
}
const step = dotSize * 1.6;
const dotR = dotSize * 0.55;
for (let x = -halfW; x <= -halfW + fillW; x += step) {
const isWarn = x >= warnStartX;
const col = isWarn ? colWarn : colNorm;
drawLed(ctx, x, y, dotR, env.config?.CLOCK_LED_GLOW !== false, col);
}
} else {
// Norm-Anteil
const normW = over ? Math.max(0, warnStartX - (-halfW)) : fillW;
if (normW > 0) {
const glowH = h * 1.1;
ctx.fillStyle = colorWithAlpha(colNorm, 0.12);
ctx.fillRect(-halfW, y - glowH / 2, normW, glowH);
ctx.fillStyle = colorWithAlpha(colNorm, 0.08);
ctx.fillRect(-halfW, y - glowH, normW, glowH * 2);
ctx.fillStyle = colNorm;
ctx.fillRect(-halfW, y - h / 2, normW, h);
}
// Warn-Anteil
if (over) {
const warnW = Math.max(0, fillW - (warnStartX - (-halfW)));
if (warnW > 0) {
const glowH = h * 1.1;
ctx.fillStyle = colorWithAlpha(colWarn, 0.15);
ctx.fillRect(warnStartX, y - glowH / 2, warnW, glowH);
ctx.fillStyle = colorWithAlpha(colWarn, 0.1);
ctx.fillRect(warnStartX, y - glowH, warnW, glowH * 2);
ctx.fillStyle = colWarn;
ctx.fillRect(warnStartX, y - h / 2, warnW, h);
}
}
// Ticks/Labels obenauf im Balken-Modus (Farben wie PPM-DIN Prozent-Marks)
ctx.save();
const tickColOver = colorWithAlpha(tickCol, ledMode ? 1.0 : 0.6);
ctx.fillStyle = tickColOver;
ctx.font = `${Math.max(5, h * 0.8)}px ui-monospace, monospace`;
ticks.forEach((t) => {
const x = -halfW + norm(t) * width;
const tickLen = (ch === 1) ? h * 1.8 : h * 1.2; // unterer Balken: längere Ticks
ctx.fillRect(x - 0.5, y - h * 0.6, 1, tickLen);
if (ch === 1) {
ctx.textAlign = (t === -50 ? 'right' : 'left');
ctx.textBaseline = 'bottom';
const labelX = t === -50 ? (x - h * 0.2) : (x + h * 0.2);
ctx.fillText(String(t), labelX, y + tickLen + h * 0.15);
}
});
ctx.restore();
}
}
ctx.restore();
}
function mapPpmRawToDin(raw, cfg) {
const minDb = Number.isFinite(cfg?.PPM_DIN_BOTTOM) ? cfg.PPM_DIN_BOTTOM : DIN_SCALE[0].db;
const maxDb = Number.isFinite(cfg?.PPM_DIN_TOP) ? cfg.PPM_DIN_TOP : DIN_SCALE[DIN_SCALE.length - 1].db;
if (!Number.isFinite(raw)) return minDb;
const base = Number.isFinite(cfg?.PPM_REF_DBFS_PEAK_FOR_0_DBU) ? cfg.PPM_REF_DBFS_PEAK_FOR_0_DBU : -15;
const effOff = (cfg?.PPM_DIN_MODE === 'al_minus6' ? -6 : -9) + (Number(cfg?.PPM_DIN_TRIM_DB) || 0);
const mapped = (raw - base) + effOff;
return Math.max(minDb, Math.min(maxDb, mapped));
}
function drawDotText(ctx, text, x, y, glyphSize, dotRadius, spacingFactor = 0.9, colonDotRadiusOverride = null, extraGapIndices = [], glow = true, color = 'rgb(255,0,0)', glyphHeightOverride = null) {
let cursorX = x;
const glyphW = glyphSize;
const glyphH = (glyphHeightOverride || glyphSize) * 1.2;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
const useDot = (ch === ':' && colonDotRadiusOverride !== null) ? colonDotRadiusOverride : dotRadius;
drawDotChar(ctx, ch, cursorX, y, glyphW, glyphH, useDot, glow, color);
cursorX += glyphW * spacingFactor;
if (extraGapIndices.includes(i)) cursorX += glyphW / DOT_COLS;
}
}
function measureTextLayout(text, glyphSize, spacingFactor, extraGapIndices = []) {
const glyphW = glyphSize;
let cursor = 0;
let colonCenter = null;
for (let i = 0; i < text.length; i++) {
if (text[i] === ':') colonCenter = cursor + glyphW * 0.5;
cursor += glyphW * spacingFactor;
if (extraGapIndices.includes(i)) cursor += glyphW / DOT_COLS;
}
if (colonCenter === null) colonCenter = cursor / 2;
return { width: cursor, colonCenter };
}
function drawDotChar(ctx, ch, x, y, w, h, dotRadius, glow = true, color = 'rgb(255,0,0)') {
if (!dotRadius) return;
const rows = DOT_FONT[ch] || DOT_FONT['0'];
const cols = rows[0].length;
const r = Math.min(dotRadius, Math.max(1, Math.min(w, h) * 0.04));
const cellW = w / cols;
const cellH = h / rows.length;
for (let row = 0; row < rows.length; row++) {
for (let col = 0; col < cols; col++) {
if (rows[row][col] === '1') {
const cx = x + col * cellW + cellW / 2;
const cy = y + row * cellH + cellH / 2;
drawLed(ctx, cx, cy, r, glow, color);
}
}
}
}
function drawLed(ctx, cx, cy, r, glow = true, color = '#ff0000') {
if (r <= 0) return;
if (glow) {
const haloR = r * 2.2;
const midR = r * 1.4;
const col = normalizeColor(color);
ctx.fillStyle = colorWithAlpha(col, 0.08);
ctx.beginPath();
ctx.arc(cx, cy, haloR, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = colorWithAlpha(col, 0.35);
ctx.beginPath();
ctx.arc(cx, cy, midR, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = normalizeColor(color);
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
}
function pad2(n) {
return n < 10 ? `0${n}` : String(n);
}
function isSecondPulseActive(date, pulseMs = 500) {
return date.getMilliseconds() < pulseMs;
}
function normalizeColor(val) {
if (typeof val !== 'string' || !val.trim()) return '#ff0000';
return val.trim();
}
function colorWithAlpha(color, alpha) {
const c = normalizeColor(color);
if (/^#([0-9a-fA-F]{6})$/.test(c)) {
const r = parseInt(c.slice(1, 3), 16);
const g = parseInt(c.slice(3, 5), 16);
const b = parseInt(c.slice(5, 7), 16);
return `rgba(${r},${g},${b},${alpha})`;
}
if (/^rgb\(/i.test(c)) {
const nums = c.match(/(\d+\.?\d*)/g)?.slice(0, 3) || [255, 0, 0];
return `rgba(${nums[0]},${nums[1]},${nums[2]},${alpha})`;
}
return `rgba(255,0,0,${alpha})`;
}