Add multi-gauge panel layouts (2 side-by-side, 3 triangle, 3 row)
This commit is contained in:
+339
-94
@@ -130,6 +130,34 @@
|
|||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
<h1>⚡ EV GAUGE DESIGNER</h1>
|
<h1>⚡ EV GAUGE DESIGNER</h1>
|
||||||
|
|
||||||
|
<!-- PANEL LAYOUT -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Panel Layout</h2>
|
||||||
|
<div class="row">
|
||||||
|
<label>Layout</label>
|
||||||
|
<select id="panelLayout">
|
||||||
|
<option value="single">Single gauge</option>
|
||||||
|
<option value="2h">2 gauges — side by side</option>
|
||||||
|
<option value="3triangle">3 gauges — L / R / Bottom</option>
|
||||||
|
<option value="3row">3 gauges — row</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="gaugeTabsRow" style="display:none">
|
||||||
|
<div class="row" style="margin-top:4px">
|
||||||
|
<label>Editing gauge</label>
|
||||||
|
<div style="display:flex;gap:4px;flex:1">
|
||||||
|
<button class="mode-btn active" id="gaugeTab0">G 1</button>
|
||||||
|
<button class="mode-btn" id="gaugeTab1">G 2</button>
|
||||||
|
<button class="mode-btn" id="gaugeTab2">G 3</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="panelBgRow" class="row" style="display:none">
|
||||||
|
<label>Panel background</label>
|
||||||
|
<input type="color" id="panelBgColor" value="#1a1a1a">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- PHYSICAL SIZE -->
|
<!-- PHYSICAL SIZE -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Physical Size</h2>
|
<h2>Physical Size</h2>
|
||||||
@@ -633,21 +661,21 @@ bindRange('logoY', 'logoYVal');
|
|||||||
bindRange('logoW', 'logoWVal');
|
bindRange('logoW', 'logoWVal');
|
||||||
bindRange('logoOpacity', 'logoOpacityVal');
|
bindRange('logoOpacity', 'logoOpacityVal');
|
||||||
|
|
||||||
function setLogo(img, name) {
|
function setLogo(img, name, silent = false) {
|
||||||
logoImg = img;
|
logoImg = img;
|
||||||
$('logoThumb').src = img.src;
|
$('logoThumb').src = img.src;
|
||||||
$('logoFileName').textContent = name;
|
$('logoFileName').textContent = name;
|
||||||
$('logoThumbWrap').style.display = 'flex';
|
$('logoThumbWrap').style.display = 'flex';
|
||||||
$('logoDropZone').style.display = 'none';
|
$('logoDropZone').style.display = 'none';
|
||||||
drawGauge();
|
if (!silent) drawGauge();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLogo() {
|
function clearLogo(silent = false) {
|
||||||
logoImg = null;
|
logoImg = null;
|
||||||
$('logoThumb').src = '';
|
$('logoThumb').src = '';
|
||||||
$('logoThumbWrap').style.display = 'none';
|
$('logoThumbWrap').style.display = 'none';
|
||||||
$('logoDropZone').style.display = 'block';
|
$('logoDropZone').style.display = 'block';
|
||||||
drawGauge();
|
if (!silent) drawGauge();
|
||||||
}
|
}
|
||||||
|
|
||||||
$('logoFileInput').addEventListener('change', e => {
|
$('logoFileInput').addEventListener('change', e => {
|
||||||
@@ -683,6 +711,169 @@ dz.addEventListener('drop', e => {
|
|||||||
|
|
||||||
$('clearLogoBtn').addEventListener('click', clearLogo);
|
$('clearLogoBtn').addEventListener('click', clearLogo);
|
||||||
|
|
||||||
|
// ── multi-gauge state ─────────────────────────────────────────────────────────
|
||||||
|
let numGauges = 1;
|
||||||
|
let gaugeLayout = 'single';
|
||||||
|
let activeGaugeIdx = 0;
|
||||||
|
const gaugeStates = [null, null, null];
|
||||||
|
|
||||||
|
function buildConfigFromState(s) {
|
||||||
|
if (!s) return null;
|
||||||
|
const diamVal = parseFloat(s.diameter) || 3.5;
|
||||||
|
const diamUnit = s.diamUnit || 'in';
|
||||||
|
const diamIn = diamUnit === 'mm' ? diamVal / 25.4 : diamVal;
|
||||||
|
const dpi = parseInt(s.printDpi) || 300;
|
||||||
|
const px = Math.round(diamIn * dpi);
|
||||||
|
return {
|
||||||
|
diamIn, dpi, px,
|
||||||
|
mode: s.gaugeMode || 'standard',
|
||||||
|
startAngle: parseFloat(s.startAngle) || 0,
|
||||||
|
sweepAngle: parseFloat(s.sweepAngle) || 270,
|
||||||
|
sweepDir: s.sweepDir || 'cw',
|
||||||
|
scaleMin: parseFloat(s.scaleMin) || 0,
|
||||||
|
scaleMax: parseFloat(s.scaleMax) || 100,
|
||||||
|
centerAngle: parseFloat(s.centerAngle) || 0,
|
||||||
|
centerValue: parseFloat(s.centerValue) || 0,
|
||||||
|
leftSweep: parseFloat(s.leftSweep) || 120,
|
||||||
|
rightSweep: parseFloat(s.rightSweep) || 120,
|
||||||
|
leftMax: parseFloat(s.leftMax) || -60,
|
||||||
|
rightMax: parseFloat(s.rightMax) || 60,
|
||||||
|
leftInterval: parseFloat(s.leftInterval) || 20,
|
||||||
|
rightInterval: parseFloat(s.rightInterval) || 20,
|
||||||
|
arcRadius: parseFloat(s.arcRadius) / 100,
|
||||||
|
majorInterval: parseFloat(s.majorInterval) || 10,
|
||||||
|
minorDivisions: parseInt(s.minorDivisions) || 4,
|
||||||
|
majorLen: parseFloat(s.majorLen) / 100,
|
||||||
|
minorLen: parseFloat(s.minorLen) / 100,
|
||||||
|
majorWidth: parseFloat(s.majorWidth) || 1,
|
||||||
|
minorWidth: parseFloat(s.minorWidth) || 0.5,
|
||||||
|
tickColor: s.tickColor || '#ffffff',
|
||||||
|
showNumbers: s.showNumbers === true || s.showNumbers === 'true',
|
||||||
|
numOffset: parseFloat(s.numOffset) / 100,
|
||||||
|
numFontSize: parseFloat(s.numFontSize) / 100,
|
||||||
|
numFont: s.numFont || 'Arial',
|
||||||
|
numWeight: s.numWeight || '400',
|
||||||
|
labelFont: s.labelFont || 'Arial',
|
||||||
|
labelWeight: s.labelWeight || '700',
|
||||||
|
gaugeTitle: s.gaugeTitle || '',
|
||||||
|
titleY: parseFloat(s.titleY) / 100,
|
||||||
|
titleSize: parseFloat(s.titleSize) / 100,
|
||||||
|
gaugeSubtitle: s.gaugeSubtitle || '',
|
||||||
|
subtitleY: parseFloat(s.subtitleY) / 100,
|
||||||
|
subtitleSize: parseFloat(s.subtitleSize) / 100,
|
||||||
|
labelColor: s.labelColor || '#ffffff',
|
||||||
|
faceColor: s.faceColor || '#000000',
|
||||||
|
faceGradient: s.faceGradient === true || s.faceGradient === 'true',
|
||||||
|
gradientColor: s.gradientColor || '#1a1a3e',
|
||||||
|
borderColor: s.borderColor || '#888888',
|
||||||
|
borderWidth: parseFloat(s.borderWidth) / 100,
|
||||||
|
centerDotColor: s.centerDotColor || '#e94560',
|
||||||
|
centerDotSize: parseFloat(s.centerDotSize) / 100,
|
||||||
|
zones: s.zones || [],
|
||||||
|
logoImg: s._logoImg || null,
|
||||||
|
logoX: parseFloat(s.logoX) / 100,
|
||||||
|
logoY: parseFloat(s.logoY) / 100,
|
||||||
|
logoW: parseFloat(s.logoW) / 100,
|
||||||
|
logoOpacity: parseFloat(s.logoOpacity) / 100,
|
||||||
|
logoBlend: s.logoBlend || 'source-over'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureGaugeState(idx) {
|
||||||
|
const state = { gaugeMode, sweepDir, zones: JSON.parse(JSON.stringify(zones)), _logoImg: logoImg };
|
||||||
|
document.querySelectorAll('#sidebar input, #sidebar select').forEach(el => {
|
||||||
|
if (!el.id || el.type === 'file') return;
|
||||||
|
state[el.id] = el.type === 'checkbox' ? el.checked : el.value;
|
||||||
|
});
|
||||||
|
if (logoImg) { state.logoDataUrl = logoImg.src; state.logoFileName = $('logoFileName').textContent; }
|
||||||
|
else { state.logoDataUrl = null; state.logoFileName = ''; }
|
||||||
|
gaugeStates[idx] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreGaugeState(idx) {
|
||||||
|
const data = gaugeStates[idx];
|
||||||
|
if (!data) return;
|
||||||
|
document.querySelectorAll('#sidebar input, #sidebar select').forEach(el => {
|
||||||
|
if (!el.id || el.type === 'file' || !(el.id in data)) return;
|
||||||
|
if (el.type === 'checkbox') el.checked = data[el.id];
|
||||||
|
else el.value = data[el.id];
|
||||||
|
});
|
||||||
|
document.querySelectorAll('#sidebar input[type=range]').forEach(el => el.dispatchEvent(new Event('input')));
|
||||||
|
gaugeMode = data.gaugeMode || 'standard';
|
||||||
|
if (gaugeMode === 'center') {
|
||||||
|
$('modeCtr').classList.add('active'); $('modeStd').classList.remove('active');
|
||||||
|
$('ctrFields').style.display = ''; $('stdFields').style.display = 'none';
|
||||||
|
$('ctrTickNote').style.display = ''; $('stdTickNote').style.display = 'none';
|
||||||
|
$('stdMajorInterval').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
$('modeStd').classList.add('active'); $('modeCtr').classList.remove('active');
|
||||||
|
$('stdFields').style.display = ''; $('ctrFields').style.display = 'none';
|
||||||
|
$('stdTickNote').style.display = ''; $('ctrTickNote').style.display = 'none';
|
||||||
|
$('stdMajorInterval').style.display = '';
|
||||||
|
}
|
||||||
|
sweepDir = data.sweepDir || 'cw';
|
||||||
|
if (sweepDir === 'ccw') {
|
||||||
|
$('dirCCW').classList.add('active'); $('dirCW').classList.remove('active');
|
||||||
|
} else {
|
||||||
|
$('dirCW').classList.add('active'); $('dirCCW').classList.remove('active');
|
||||||
|
}
|
||||||
|
zones = data.zones || [];
|
||||||
|
renderZones();
|
||||||
|
logoImg = data._logoImg || null;
|
||||||
|
if (logoImg) setLogo(logoImg, data.logoFileName || 'logo', true);
|
||||||
|
else clearLogo(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchGauge(newIdx) {
|
||||||
|
captureGaugeState(activeGaugeIdx);
|
||||||
|
activeGaugeIdx = newIdx;
|
||||||
|
restoreGaugeState(activeGaugeIdx);
|
||||||
|
updateGaugeTabs();
|
||||||
|
drawGauge();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateGaugeTabs() {
|
||||||
|
[0, 1, 2].forEach(i => {
|
||||||
|
const tab = $(`gaugeTab${i}`);
|
||||||
|
if (!tab) return;
|
||||||
|
tab.classList.toggle('active', i === activeGaugeIdx);
|
||||||
|
tab.style.display = i < numGauges ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePanelUI() {
|
||||||
|
const multi = numGauges > 1;
|
||||||
|
$('gaugeTabsRow').style.display = multi ? '' : 'none';
|
||||||
|
$('panelBgRow').style.display = multi ? '' : 'none';
|
||||||
|
updateGaugeTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeLayout(px, layout, scale) {
|
||||||
|
const r = Math.round(px * scale / 2);
|
||||||
|
const pad = Math.max(4, Math.round(r * 0.08));
|
||||||
|
switch (layout) {
|
||||||
|
case '2h':
|
||||||
|
return { w: r * 4 + pad, h: r * 2,
|
||||||
|
pos: [{ cx: r, cy: r }, { cx: r * 3 + pad, cy: r }] };
|
||||||
|
case '3triangle':
|
||||||
|
return { w: r * 4 + pad, h: r * 4 + pad,
|
||||||
|
pos: [
|
||||||
|
{ cx: r, cy: r },
|
||||||
|
{ cx: r * 3 + pad, cy: r },
|
||||||
|
{ cx: r * 2 + Math.round(pad / 2), cy: r * 3 + pad }
|
||||||
|
]};
|
||||||
|
case '3row':
|
||||||
|
return { w: r * 6 + pad * 2, h: r * 2,
|
||||||
|
pos: [
|
||||||
|
{ cx: r, cy: r },
|
||||||
|
{ cx: r * 3 + pad, cy: r },
|
||||||
|
{ cx: r * 5 + pad * 2, cy: r }
|
||||||
|
]};
|
||||||
|
default:
|
||||||
|
return { w: r * 2, h: r * 2, pos: [{ cx: r, cy: r }] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── getConfig ─────────────────────────────────────────────────────────────────
|
// ── getConfig ─────────────────────────────────────────────────────────────────
|
||||||
function getConfig() {
|
function getConfig() {
|
||||||
const diamVal = parseFloat($('diameter').value) || 3.5;
|
const diamVal = parseFloat($('diameter').value) || 3.5;
|
||||||
@@ -889,15 +1080,9 @@ function renderCenterSide(ctx, cfg, cx, cy, r, scalePx, side) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── main render ───────────────────────────────────────────────────────────────
|
// ── main render ───────────────────────────────────────────────────────────────
|
||||||
function renderGaugeToCanvas(canvas, cfg, scale = 1) {
|
function renderGaugeFace(ctx, cfg, cx, cy, r) {
|
||||||
const size = Math.round(cfg.px * scale);
|
const d = r * 2;
|
||||||
canvas.width = size;
|
const scalePx = d / cfg.px;
|
||||||
canvas.height = size;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const cx = size / 2, cy = size / 2, r = size / 2;
|
|
||||||
const scalePx = size / cfg.px;
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, size, size);
|
|
||||||
|
|
||||||
// face fill
|
// face fill
|
||||||
ctx.save();
|
ctx.save();
|
||||||
@@ -905,14 +1090,14 @@ function renderGaugeToCanvas(canvas, cfg, scale = 1) {
|
|||||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
if (cfg.faceGradient) {
|
if (cfg.faceGradient) {
|
||||||
const grad = ctx.createRadialGradient(cx, cy * 0.6, 0, cx, cy, r);
|
const grad = ctx.createRadialGradient(cx, cy - r * 0.4, 0, cx, cy, r);
|
||||||
grad.addColorStop(0, cfg.gradientColor);
|
grad.addColorStop(0, cfg.gradientColor);
|
||||||
grad.addColorStop(1, cfg.faceColor);
|
grad.addColorStop(1, cfg.faceColor);
|
||||||
ctx.fillStyle = grad;
|
ctx.fillStyle = grad;
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = cfg.faceColor;
|
ctx.fillStyle = cfg.faceColor;
|
||||||
}
|
}
|
||||||
ctx.fillRect(0, 0, size, size);
|
ctx.fillRect(cx - r, cy - r, d, d);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// border ring
|
// border ring
|
||||||
@@ -955,14 +1140,13 @@ function renderGaugeToCanvas(canvas, cfg, scale = 1) {
|
|||||||
|
|
||||||
// logo
|
// logo
|
||||||
if (cfg.logoImg) {
|
if (cfg.logoImg) {
|
||||||
const lw = size * cfg.logoW;
|
const lw = d * cfg.logoW;
|
||||||
const lh = lw * (cfg.logoImg.naturalHeight / cfg.logoImg.naturalWidth);
|
const lh = lw * (cfg.logoImg.naturalHeight / cfg.logoImg.naturalWidth);
|
||||||
const lx = size * cfg.logoX - lw / 2;
|
const lx = (cx - r) + d * cfg.logoX - lw / 2;
|
||||||
const ly = size * cfg.logoY - lh / 2;
|
const ly = (cy - r) + d * cfg.logoY - lh / 2;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalAlpha = cfg.logoOpacity;
|
ctx.globalAlpha = cfg.logoOpacity;
|
||||||
ctx.globalCompositeOperation = cfg.logoBlend;
|
ctx.globalCompositeOperation = cfg.logoBlend;
|
||||||
// clip to circle so logo can't bleed outside face
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
@@ -979,13 +1163,49 @@ function renderGaugeToCanvas(canvas, cfg, scale = 1) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderGaugeToCanvas(canvas, cfg, scale = 1) {
|
||||||
|
const size = Math.round(cfg.px * scale);
|
||||||
|
canvas.width = size;
|
||||||
|
canvas.height = size;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, size, size);
|
||||||
|
renderGaugeFace(ctx, cfg, size / 2, size / 2, size / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPanelToCanvas(canvas, scale = 1) {
|
||||||
|
captureGaugeState(activeGaugeIdx);
|
||||||
|
if (numGauges === 1) {
|
||||||
|
renderGaugeToCanvas(canvas, buildConfigFromState(gaugeStates[0]), scale);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cfg0 = buildConfigFromState(gaugeStates[0]);
|
||||||
|
const layout = computeLayout(cfg0.px, gaugeLayout, scale);
|
||||||
|
canvas.width = layout.w;
|
||||||
|
canvas.height = layout.h;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.fillStyle = $('panelBgColor').value;
|
||||||
|
ctx.fillRect(0, 0, layout.w, layout.h);
|
||||||
|
const r = Math.round(cfg0.px * scale / 2);
|
||||||
|
for (let i = 0; i < numGauges; i++) {
|
||||||
|
const cfg = buildConfigFromState(gaugeStates[i]);
|
||||||
|
if (!cfg) continue;
|
||||||
|
renderGaugeFace(ctx, cfg, layout.pos[i].cx, layout.pos[i].cy, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function drawGauge() {
|
function drawGauge() {
|
||||||
const cfg = getConfig();
|
|
||||||
const scaleF = parseInt($('previewScale').value) / 100;
|
const scaleF = parseInt($('previewScale').value) / 100;
|
||||||
renderGaugeToCanvas($('gaugeCanvas'), cfg, scaleF);
|
renderPanelToCanvas($('gaugeCanvas'), scaleF);
|
||||||
const displayPx = Math.round(cfg.px * scaleF);
|
const cfg0 = buildConfigFromState(gaugeStates[0]);
|
||||||
|
if (!cfg0) return;
|
||||||
|
if (numGauges === 1) {
|
||||||
|
const displayPx = Math.round(cfg0.px * scaleF);
|
||||||
$('info').textContent =
|
$('info').textContent =
|
||||||
`Preview: ${displayPx}×${displayPx}px | Print size: ${cfg.diamIn.toFixed(3)}" (${(cfg.diamIn*25.4).toFixed(1)}mm) | Full res: ${cfg.px}×${cfg.px}px @ ${cfg.dpi}dpi`;
|
`Preview: ${displayPx}×${displayPx}px | Print size: ${cfg0.diamIn.toFixed(3)}" (${(cfg0.diamIn*25.4).toFixed(1)}mm) | Full res: ${cfg0.px}×${cfg0.px}px @ ${cfg0.dpi}dpi`;
|
||||||
|
} else {
|
||||||
|
$('info').textContent =
|
||||||
|
`${numGauges} gauges — each ${cfg0.diamIn.toFixed(3)}" dia (${(cfg0.diamIn*25.4).toFixed(1)}mm) @ ${cfg0.dpi}dpi`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// listen to all sidebar inputs
|
// listen to all sidebar inputs
|
||||||
@@ -994,49 +1214,82 @@ document.querySelectorAll('#sidebar input, #sidebar select').forEach(el => {
|
|||||||
el.addEventListener('change', drawGauge);
|
el.addEventListener('change', drawGauge);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// panel layout + gauge tabs
|
||||||
|
$('panelLayout').addEventListener('change', () => {
|
||||||
|
captureGaugeState(activeGaugeIdx);
|
||||||
|
gaugeLayout = $('panelLayout').value;
|
||||||
|
numGauges = gaugeLayout === 'single' ? 1 : gaugeLayout === '2h' ? 2 : 3;
|
||||||
|
if (activeGaugeIdx >= numGauges) {
|
||||||
|
activeGaugeIdx = 0;
|
||||||
|
restoreGaugeState(0);
|
||||||
|
}
|
||||||
|
updatePanelUI();
|
||||||
|
drawGauge();
|
||||||
|
});
|
||||||
|
|
||||||
|
[0, 1, 2].forEach(i => {
|
||||||
|
$(`gaugeTab${i}`).addEventListener('click', () => switchGauge(i));
|
||||||
|
});
|
||||||
|
|
||||||
// ── export PDF ────────────────────────────────────────────────────────────────
|
// ── export PDF ────────────────────────────────────────────────────────────────
|
||||||
$('exportPdfBtn').addEventListener('click', () => {
|
$('exportPdfBtn').addEventListener('click', () => {
|
||||||
const cfg = getConfig();
|
captureGaugeState(activeGaugeIdx);
|
||||||
|
const cfg0 = buildConfigFromState(gaugeStates[0]);
|
||||||
const fullCanvas = document.createElement('canvas');
|
const fullCanvas = document.createElement('canvas');
|
||||||
renderGaugeToCanvas(fullCanvas, cfg, 1);
|
renderPanelToCanvas(fullCanvas, 1);
|
||||||
const imgData = fullCanvas.toDataURL('image/png');
|
const imgData = fullCanvas.toDataURL('image/png');
|
||||||
const dIn = cfg.diamIn;
|
const dIn = cfg0.diamIn;
|
||||||
const pdf = new jsPDF({ orientation: 'portrait', unit: 'in', format: [dIn + 0.5, dIn + 0.5] });
|
let pdfW, pdfH;
|
||||||
pdf.addImage(imgData, 'PNG', 0.25, 0.25, dIn, dIn);
|
if (numGauges === 1) {
|
||||||
// registration marks
|
pdfW = dIn + 0.5; pdfH = dIn + 0.5;
|
||||||
|
} else {
|
||||||
|
const lay = computeLayout(cfg0.px, gaugeLayout, 1);
|
||||||
|
pdfW = (lay.w / cfg0.px) * dIn + 0.5;
|
||||||
|
pdfH = (lay.h / cfg0.px) * dIn + 0.5;
|
||||||
|
}
|
||||||
|
const pdf = new jsPDF({ orientation: pdfW >= pdfH ? 'landscape' : 'portrait', unit: 'in', format: [pdfW, pdfH] });
|
||||||
|
pdf.addImage(imgData, 'PNG', 0.25, 0.25, pdfW - 0.5, pdfH - 0.5);
|
||||||
pdf.setDrawColor(180, 180, 180);
|
pdf.setDrawColor(180, 180, 180);
|
||||||
pdf.setLineWidth(0.01);
|
pdf.setLineWidth(0.01);
|
||||||
const m = 0.05, bl = 0.15, pw = dIn + 0.5, ph = dIn + 0.5;
|
const m = 0.05, bl = 0.15;
|
||||||
[[m,m],[pw-m,m],[m,ph-m],[pw-m,ph-m]].forEach(([x,y]) => {
|
[[m,m],[pdfW-m,m],[m,pdfH-m],[pdfW-m,pdfH-m]].forEach(([x,y]) => {
|
||||||
pdf.line(x-bl, y, x+bl, y);
|
pdf.line(x-bl, y, x+bl, y);
|
||||||
pdf.line(x, y-bl, x, y+bl);
|
pdf.line(x, y-bl, x, y+bl);
|
||||||
});
|
});
|
||||||
pdf.save(`${cfg.gaugeTitle || 'gauge'}-${dIn.toFixed(2)}in.pdf`);
|
pdf.save(`${cfg0.gaugeTitle || 'gauge-panel'}-${dIn.toFixed(2)}in.pdf`);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('exportPngBtn').addEventListener('click', () => {
|
$('exportPngBtn').addEventListener('click', () => {
|
||||||
const cfg = getConfig();
|
captureGaugeState(activeGaugeIdx);
|
||||||
|
const cfg0 = buildConfigFromState(gaugeStates[0]);
|
||||||
|
const fullCanvas = document.createElement('canvas');
|
||||||
|
renderPanelToCanvas(fullCanvas, 1);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = `${cfg.gaugeTitle || 'gauge'}-preview.png`;
|
link.download = `${cfg0.gaugeTitle || 'gauge'}-panel.png`;
|
||||||
link.href = $('gaugeCanvas').toDataURL('image/png');
|
link.href = fullCanvas.toDataURL('image/png');
|
||||||
link.click();
|
link.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── save / load design ────────────────────────────────────────────────────────
|
// ── save / load design ────────────────────────────────────────────────────────
|
||||||
$('saveDesignBtn').addEventListener('click', () => {
|
$('saveDesignBtn').addEventListener('click', () => {
|
||||||
const data = { version: 1, gaugeMode, sweepDir, zones: JSON.parse(JSON.stringify(zones)) };
|
captureGaugeState(activeGaugeIdx);
|
||||||
document.querySelectorAll('#sidebar input, #sidebar select').forEach(el => {
|
const saveData = {
|
||||||
if (!el.id || el.type === 'file') return;
|
version: 2,
|
||||||
data[el.id] = el.type === 'checkbox' ? el.checked : el.value;
|
numGauges,
|
||||||
});
|
gaugeLayout,
|
||||||
if (logoImg) {
|
panelBgColor: $('panelBgColor').value,
|
||||||
data.logoDataUrl = logoImg.src;
|
gaugeStates: gaugeStates.map(s => {
|
||||||
data.logoFileName = $('logoFileName').textContent;
|
if (!s) return null;
|
||||||
}
|
const copy = Object.assign({}, s);
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
delete copy._logoImg;
|
||||||
|
return copy;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
const name = (gaugeStates[0] && gaugeStates[0].gaugeTitle) || 'gauge';
|
||||||
|
const blob = new Blob([JSON.stringify(saveData, null, 2)], { type: 'application/json' });
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = URL.createObjectURL(blob);
|
a.href = URL.createObjectURL(blob);
|
||||||
a.download = `${$('gaugeTitle').value || 'gauge'}-design.json`;
|
a.download = `${name}-design.json`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(a.href);
|
URL.revokeObjectURL(a.href);
|
||||||
});
|
});
|
||||||
@@ -1050,59 +1303,47 @@ $('loadDesignInput').addEventListener('change', e => {
|
|||||||
reader.onload = ev => {
|
reader.onload = ev => {
|
||||||
let data;
|
let data;
|
||||||
try { data = JSON.parse(ev.target.result); } catch { alert('Invalid design file.'); return; }
|
try { data = JSON.parse(ev.target.result); } catch { alert('Invalid design file.'); return; }
|
||||||
|
applyLoadedDesign(data);
|
||||||
// restore all inputs silently (no events yet)
|
|
||||||
document.querySelectorAll('#sidebar input, #sidebar select').forEach(el => {
|
|
||||||
if (!el.id || el.type === 'file' || !(el.id in data)) return;
|
|
||||||
if (el.type === 'checkbox') el.checked = data[el.id];
|
|
||||||
else el.value = data[el.id];
|
|
||||||
});
|
|
||||||
|
|
||||||
// update range display spans by firing input events on ranges
|
|
||||||
document.querySelectorAll('#sidebar input[type=range]').forEach(el => {
|
|
||||||
el.dispatchEvent(new Event('input'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// restore gauge mode UI
|
|
||||||
gaugeMode = data.gaugeMode || 'standard';
|
|
||||||
if (gaugeMode === 'center') {
|
|
||||||
$('modeCtr').classList.add('active'); $('modeStd').classList.remove('active');
|
|
||||||
$('ctrFields').style.display = ''; $('stdFields').style.display = 'none';
|
|
||||||
$('ctrTickNote').style.display = ''; $('stdTickNote').style.display = 'none';
|
|
||||||
$('stdMajorInterval').style.display = 'none';
|
|
||||||
} else {
|
|
||||||
$('modeStd').classList.add('active'); $('modeCtr').classList.remove('active');
|
|
||||||
$('stdFields').style.display = ''; $('ctrFields').style.display = 'none';
|
|
||||||
$('stdTickNote').style.display = ''; $('ctrTickNote').style.display = 'none';
|
|
||||||
$('stdMajorInterval').style.display = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// restore sweep direction UI
|
|
||||||
sweepDir = data.sweepDir || 'cw';
|
|
||||||
if (sweepDir === 'ccw') {
|
|
||||||
$('dirCCW').classList.add('active'); $('dirCW').classList.remove('active');
|
|
||||||
} else {
|
|
||||||
$('dirCW').classList.add('active'); $('dirCCW').classList.remove('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
// restore zones
|
|
||||||
zones = data.zones || [];
|
|
||||||
renderZones();
|
|
||||||
|
|
||||||
// restore logo
|
|
||||||
if (data.logoDataUrl) {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => { setLogo(img, data.logoFileName || 'logo'); drawGauge(); };
|
|
||||||
img.src = data.logoDataUrl;
|
|
||||||
} else {
|
|
||||||
clearLogo();
|
|
||||||
drawGauge();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function applyLoadedDesign(data) {
|
||||||
|
if (data.version >= 2) {
|
||||||
|
numGauges = data.numGauges || 1;
|
||||||
|
gaugeLayout = data.gaugeLayout || 'single';
|
||||||
|
$('panelLayout').value = gaugeLayout;
|
||||||
|
if (data.panelBgColor) $('panelBgColor').value = data.panelBgColor;
|
||||||
|
const states = data.gaugeStates || [];
|
||||||
|
states.forEach((s, i) => { if (s) gaugeStates[i] = Object.assign({}, s, { _logoImg: null }); });
|
||||||
|
Promise.all(states.map((s, i) => {
|
||||||
|
if (!s || !s.logoDataUrl) return Promise.resolve();
|
||||||
|
return new Promise(res => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => { gaugeStates[i]._logoImg = img; res(); };
|
||||||
|
img.src = s.logoDataUrl;
|
||||||
|
});
|
||||||
|
})).then(() => {
|
||||||
|
activeGaugeIdx = 0;
|
||||||
|
updatePanelUI();
|
||||||
|
restoreGaugeState(0);
|
||||||
|
drawGauge();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// v1 single-gauge format (backwards compatible)
|
||||||
|
numGauges = 1; gaugeLayout = 'single';
|
||||||
|
$('panelLayout').value = 'single';
|
||||||
|
gaugeStates[0] = Object.assign({}, data, { _logoImg: null });
|
||||||
|
updatePanelUI(); activeGaugeIdx = 0;
|
||||||
|
if (data.logoDataUrl) {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => { gaugeStates[0]._logoImg = img; restoreGaugeState(0); drawGauge(); };
|
||||||
|
img.src = data.logoDataUrl;
|
||||||
|
} else { restoreGaugeState(0); drawGauge(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── preview background toggle ─────────────────────────────────────────────────
|
// ── preview background toggle ─────────────────────────────────────────────────
|
||||||
const previewArea = $('preview-area');
|
const previewArea = $('preview-area');
|
||||||
|
|
||||||
@@ -1124,6 +1365,10 @@ bgCustomInput.addEventListener('input', () => {
|
|||||||
});
|
});
|
||||||
bgCustomSwatch.addEventListener('click', () => bgCustomSwatch.classList.add('active'));
|
bgCustomSwatch.addEventListener('click', () => bgCustomSwatch.classList.add('active'));
|
||||||
|
|
||||||
|
// initialize all 3 gauge states from current DOM defaults, then draw
|
||||||
|
captureGaugeState(0);
|
||||||
|
gaugeStates[1] = Object.assign({}, gaugeStates[0], { _logoImg: null, logoDataUrl: null, logoFileName: '' });
|
||||||
|
gaugeStates[2] = Object.assign({}, gaugeStates[0], { _logoImg: null, logoDataUrl: null, logoFileName: '' });
|
||||||
drawGauge();
|
drawGauge();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user