/home/simp499cl/public_html/notas-prechequeo
Edit: /home/simp499cl/public_html/notas-prechequeo/gauges-inspeccion.js (19166B)
const defaults = {
red: '#E63223',
orange: '#F97317',
green: '#22C55E',
showTicks: false,
scoreUsesBlackColor: false,
arcWidth: 18,
arcCap: 'round',
scoreSize: 42,
scoreY: 150,
labelSize: 10,
labelY: 178,
animDuration: 6500,
needleLength: 60,
needleBaseWidth: 6,
needleTipWidth: 2,
inspectionGaugeSize: 150,
finalGaugeSize: 200,
shadowBlur: 14,
shadowOpacity: 12,
shadowY: 5,
shadowSpread: 0,
animateUpdates: true
};
const inspections = [
{ id: 'mechanical', name: 'Inspección mecánica', score: 5.5 },
{ id: 'body', name: 'Inspección Carrocería', score: 6.2 },
{ id: 'safety', name: 'Inspección Seguridad', score: 4.8 },
{ id: 'drive', name: 'Prueba de manejo', score: 6.5 }
];
const state = structuredClone(defaults);
const scoreState = Object.fromEntries(inspections.map(i => [i.id, i.score]));
const gaugeBoard = document.getElementById('gaugeBoard');
const scoreControls = document.getElementById('scoreControls');
const START_ANGLE = 140;
const SWEEP = 260;
const SCORE_STEP_ANGLE = SWEEP / 6;
const ZERO_ANGLE = START_ANGLE - SCORE_STEP_ANGLE;
const CX = 100;
const CY = 100;
const R = 72;
function clampScore(v) { return Math.max(1, Math.min(7, Number(v))); }
function angleForScore(score) { return START_ANGLE + ((clampScore(score) - 1) / 6) * SWEEP; }
function angleForAnimatedScore(score) {
const v = Math.max(0, Math.min(7, Number(score)));
if (v <= 1) return ZERO_ANGLE + v * (START_ANGLE - ZERO_ANGLE);
return angleForScore(v);
}
function pointAt(angleDeg, radius = R) {
const rad = angleDeg * Math.PI / 180;
return { x: CX + Math.cos(rad) * radius, y: CY + Math.sin(rad) * radius };
}
function arcPath(startDeg, endDeg, radius = R) {
const start = pointAt(startDeg, radius);
const end = pointAt(endDeg, radius);
const large = Math.abs(endDeg - startDeg) > 180 ? 1 : 0;
return `M ${start.x.toFixed(2)} ${start.y.toFixed(2)} A ${radius} ${radius} 0 ${large} 1 ${end.x.toFixed(2)} ${end.y.toFixed(2)}`;
}
function statusForScore(score) {
if (score < 3) return { label: 'CRITICO', color: state.red };
if (score < 4) return { label: 'DEFICIENTE', color: state.orange };
if (score < 5) return { label: 'REGULAR', color: state.orange };
if (score < 6) return { label: 'BUENO', color: state.green };
return { label: 'OPTIMO', color: state.green };
}
function finalStatusForScore(score) {
if (score < 3) return { label: 'CALIFICACIÓN MUY BAJA', color: state.red };
if (score < 4) return { label: 'CALIFICACIÓN BAJA', color: state.orange };
if (score < 5) return { label: 'CALIFICACIÓN MEDIA', color: state.orange };
if (score < 6) return { label: 'CALIFICACIÓN MEDIA ALTA', color: state.green };
return { label: 'CALIFICACIÓN ALTA', color: state.green };
}
function hexToRgba(hex, alpha = .13) {
const clean = hex.replace('#', '');
const normalized = clean.length === 3 ? clean.split('').map(x => x + x).join('') : clean;
const n = parseInt(normalized, 16);
const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
function fmt(score) { return Number(score).toFixed(1); }
function averageScore() {
const vals = Object.values(scoreState);
return vals.reduce((a, b) => a + b, 0) / vals.length;
}
function buildGaugeCard(id, title, score, summary = false) {
const status = statusForScore(score);
const finalBand = summary ? finalStatusForScore(score) : null;
const card = document.createElement('article');
card.className = `gauge-card${summary ? ' summary' : ''}`;
card.dataset.gaugeId = id;
card.innerHTML = `
${summary ? `
${finalBand.label}
` : ''}
`;
return card;
}
function renderGauges() {
gaugeBoard.innerHTML = '';
const categorySection = document.createElement('div');
categorySection.className = 'gauge-category-section';
const categoryGrid = document.createElement('div');
categoryGrid.className = 'gauge-category-grid';
inspections.forEach(i => categoryGrid.appendChild(buildGaugeCard(i.id, i.name, scoreState[i.id], false)));
categorySection.appendChild(categoryGrid);
const finalSection = document.createElement('div');
finalSection.className = 'gauge-final-section';
finalSection.appendChild(buildGaugeCard('average', 'Nota Final', averageScore(), true));
gaugeBoard.append(categorySection, finalSection);
gaugeBoard.querySelectorAll('.gauge-card').forEach(card => paintGauge(card, true));
}
function renderScoreControls() {
scoreControls.innerHTML = '';
inspections.forEach(item => {
const row = document.createElement('label');
row.className = 'score-row';
row.innerHTML = `
${item.name}
`;
scoreControls.appendChild(row);
});
scoreControls.querySelectorAll('input[type="range"]').forEach(input => {
input.addEventListener('input', e => {
const id = e.target.dataset.scoreId;
scoreState[id] = clampScore(e.target.value);
e.target.nextElementSibling.textContent = fmt(scoreState[id]);
updateSingleGauge(id);
updateSingleGauge('average');
});
});
}
function setupTicks(svg) {
const markLayer = svg.querySelector('.tick-mark-layer');
const numberLayer = svg.querySelector('.tick-number-layer');
markLayer.innerHTML = '';
numberLayer.innerHTML = '';
for (let n = 1; n <= 7; n++) {
const angle = angleForScore(n);
const p1 = pointAt(angle, 52), p2 = pointAt(angle, 58);
const mark = document.createElementNS('http://www.w3.org/2000/svg', 'line');
mark.setAttribute('class', 'gauge-tick-mark');
mark.setAttribute('x1', p1.x.toFixed(2));
mark.setAttribute('y1', p1.y.toFixed(2));
mark.setAttribute('x2', p2.x.toFixed(2));
mark.setAttribute('y2', p2.y.toFixed(2));
markLayer.appendChild(mark);
if (state.showTicks) {
const p = pointAt(angle, 43);
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('class', 'gauge-tick');
text.setAttribute('x', p.x.toFixed(2));
text.setAttribute('y', p.y.toFixed(2));
text.textContent = n;
numberLayer.appendChild(text);
}
}
}
function needlePoints(length) {
const baseHalf = state.needleBaseWidth / 2;
const tipHalf = state.needleTipWidth / 2;
const tipX = CX + length;
return [
[CX, CY - baseHalf],
[tipX, CY - tipHalf],
[tipX, CY + tipHalf],
[CX, CY + baseHalf]
].map(p => p.join(',')).join(' ');
}
function applyGaugeSizingAndShadow(card) {
const summary = card.dataset.gaugeId === 'average';
const size = summary ? state.finalGaugeSize : state.inspectionGaugeSize;
const wrap = card.querySelector('.gauge-wrap');
const face = card.querySelector('.gauge-face');
wrap.style.setProperty('--gauge-size', `${size}px`);
card.style.setProperty('--gauge-size', `${size}px`);
face.style.boxShadow = `0 ${state.shadowY}px ${state.shadowBlur}px ${state.shadowSpread}px rgba(0,0,0,${state.shadowOpacity / 100})`;
}
function paintGauge(card, firstPaint = false) {
const id = card.dataset.gaugeId;
const summary = id === 'average';
const score = summary ? averageScore() : scoreState[id];
const svg = card.querySelector('svg');
const arcs = svg.querySelectorAll('.arc');
const segments = [
{ el: arcs[0], from: angleForScore(1), to: angleForScore(3), color: state.red },
{ el: arcs[1], from: angleForScore(3), to: angleForScore(5), color: state.orange },
{ el: arcs[2], from: angleForScore(5), to: angleForScore(7), color: state.green }
];
segments.forEach(s => {
s.el.setAttribute('d', arcPath(s.from, s.to));
s.el.setAttribute('stroke', s.color);
s.el.setAttribute('stroke-width', state.arcWidth);
s.el.setAttribute('stroke-linecap', 'butt');
});
const capStart = svg.querySelector('.arc-cap-start');
const capEnd = svg.querySelector('.arc-cap-end');
const startPoint = pointAt(angleForScore(1));
const endPoint = pointAt(angleForScore(7));
for (const [cap, point, color] of [[capStart, startPoint, state.red], [capEnd, endPoint, state.green]]) {
cap.setAttribute('cx', point.x.toFixed(2));
cap.setAttribute('cy', point.y.toFixed(2));
cap.setAttribute('r', (state.arcWidth / 2).toFixed(2));
cap.setAttribute('fill', color);
cap.style.display = state.arcCap === 'round' ? '' : 'none';
}
setupTicks(svg);
applyGaugeSizingAndShadow(card);
const valueText = svg.querySelector('.gauge-value');
valueText.setAttribute('y', state.scoreY);
valueText.style.fontSize = `${state.scoreSize}px`;
const statusText = svg.querySelector('.gauge-status');
const statusBg = svg.querySelector('.gauge-status-bg');
statusText.setAttribute('y', state.labelY);
statusText.style.fontSize = `${state.labelSize}px`;
const needleGroup = svg.querySelector('.needle-group');
const needle = svg.querySelector('.needle');
const needleTip = svg.querySelector('.needle-tip');
const hubInner = svg.querySelector('.needle-hub-inner');
const angle = angleForScore(score);
needle.setAttribute('points', needlePoints(state.needleLength));
needleTip.setAttribute('cx', CX + state.needleLength);
needleTip.setAttribute('r', Math.max(1.5, state.needleTipWidth * 1.15));
const renderReading = (currentScore, isAnimating = false) => {
const visualScore = Math.max(0, currentScore);
const statusScore = Math.max(1, visualScore);
const status = statusForScore(statusScore);
const finalBand = summary ? finalStatusForScore(statusScore) : null;
valueText.textContent = fmt(visualScore);
valueText.style.fill = isAnimating ? status.color : (state.scoreUsesBlackColor ? '#111111' : status.color);
statusText.textContent = status.label;
statusText.style.fill = '#ffffff';
needle.style.fill = status.color;
needleTip.style.fill = status.color;
hubInner.style.fill = status.color;
if (summary) {
const ratingLabel = card.querySelector('.final-rating-label');
ratingLabel.textContent = finalBand.label;
ratingLabel.style.backgroundColor = finalBand.color;
ratingLabel.style.color = '#ffffff';
}
const badgeWidth = Math.max(48, status.label.length * state.labelSize * .68 + 18);
const badgeHeight = state.labelSize + 10;
statusBg.setAttribute('x', (100 - badgeWidth / 2).toFixed(2));
statusBg.setAttribute('y', (state.labelY - badgeHeight / 2).toFixed(2));
statusBg.setAttribute('width', badgeWidth.toFixed(2));
statusBg.setAttribute('height', badgeHeight.toFixed(2));
statusBg.setAttribute('rx', (badgeHeight / 2).toFixed(2));
statusBg.setAttribute('fill', hexToRgba(status.color, 1));
};
const currentAngle = Number(needleGroup.dataset.angle ?? ZERO_ANGLE);
const duration = firstPaint ? state.animDuration : (state.animateUpdates ? 650 : 0);
if (firstPaint) {
needleGroup.setAttribute('transform', `rotate(${ZERO_ANGLE} ${CX} ${CY})`);
needleGroup.dataset.angle = ZERO_ANGLE;
renderReading(0, true);
animateGaugeReading(needleGroup, angle, score, duration, renderReading);
} else {
renderReading(score);
animateNeedle(needleGroup, currentAngle, angle, duration);
}
svg.setAttribute('aria-label', `${card.querySelector('.gauge-title').textContent}: ${fmt(score)} de 7`);
}
function animateGaugeReading(group, toAngle, toScore, duration, onReading) {
if (group._raf) cancelAnimationFrame(group._raf);
if (!duration) {
group.setAttribute('transform', `rotate(${toAngle} ${CX} ${CY})`);
group.dataset.angle = toAngle;
onReading(toScore, false);
return;
}
const started = performance.now();
const ease = t => .5 - Math.cos(Math.PI * t) / 2;
function frame(now) {
const t = Math.min(1, (now - started) / duration);
const visibleScore = toScore * ease(t);
const angle = angleForAnimatedScore(visibleScore);
group.setAttribute('transform', `rotate(${angle} ${CX} ${CY})`);
group.dataset.angle = angle;
onReading(visibleScore, true);
if (t < 1) group._raf = requestAnimationFrame(frame);
else {
group.dataset.angle = toAngle;
onReading(toScore, false);
}
}
group._raf = requestAnimationFrame(frame);
}
function animateNeedle(group, fromAngle, toAngle, duration) {
if (group._raf) cancelAnimationFrame(group._raf);
if (!duration) {
group.setAttribute('transform', `rotate(${toAngle} ${CX} ${CY})`);
group.dataset.angle = toAngle;
return;
}
const started = performance.now();
const ease = t => .5 - Math.cos(Math.PI * t) / 2;
function frame(now) {
const t = Math.min(1, (now - started) / duration);
const a = fromAngle + (toAngle - fromAngle) * ease(t);
group.setAttribute('transform', `rotate(${a} ${CX} ${CY})`);
group.dataset.angle = a;
if (t < 1) group._raf = requestAnimationFrame(frame);
else group.dataset.angle = toAngle;
}
group._raf = requestAnimationFrame(frame);
}
function updateSingleGauge(id) {
const card = gaugeBoard.querySelector(`[data-gauge-id="${id}"]`);
if (card) paintGauge(card, false);
}
function bindRange(id, key, suffix = '') {
const input = document.getElementById(id);
const out = document.getElementById(`${id}Out`);
const update = () => {
state[key] = Number(input.value);
if (out) out.textContent = `${input.value}${suffix}`;
gaugeBoard.querySelectorAll('.gauge-card').forEach(c => paintGauge(c, false));
};
input.addEventListener('input', update);
}
function bindSettings() {
document.getElementById('redColor').addEventListener('input', e => { state.red = e.target.value; syncColors(); });
document.getElementById('orangeColor').addEventListener('input', e => { state.orange = e.target.value; syncColors(); });
document.getElementById('greenColor').addEventListener('input', e => { state.green = e.target.value; syncColors(); });
document.getElementById('showTicks').addEventListener('change', e => { state.showTicks = e.target.checked; repaintAll(); });
document.getElementById('scoreUsesBlackColor').addEventListener('change', e => { state.scoreUsesBlackColor = e.target.checked; repaintAll(); });
document.getElementById('arcCap').addEventListener('change', e => { state.arcCap = e.target.value; repaintAll(); });
document.getElementById('animateUpdates').addEventListener('change', e => { state.animateUpdates = e.target.checked; });
bindRange('arcWidth', 'arcWidth', 'px');
bindRange('scoreSize', 'scoreSize', 'px');
bindRange('scoreY', 'scoreY', '');
bindRange('labelSize', 'labelSize', 'px');
bindRange('labelY', 'labelY', '');
bindRange('inspectionGaugeSize', 'inspectionGaugeSize', 'px');
bindRange('finalGaugeSize', 'finalGaugeSize', 'px');
bindRange('shadowBlur', 'shadowBlur', 'px');
bindRange('shadowY', 'shadowY', 'px');
bindRange('shadowSpread', 'shadowSpread', 'px');
bindRange('animDuration', 'animDuration', 'ms');
bindRange('needleLength', 'needleLength', '');
bindRange('needleBaseWidth', 'needleBaseWidth', 'px');
bindRange('needleTipWidth', 'needleTipWidth', 'px');
const shadowOpacity = document.getElementById('shadowOpacity');
shadowOpacity.addEventListener('input', () => {
state.shadowOpacity = Number(shadowOpacity.value);
document.getElementById('shadowOpacityOut').textContent = `${shadowOpacity.value}%`;
repaintAll();
});
document.getElementById('randomBtn').addEventListener('click', () => {
inspections.forEach(i => scoreState[i.id] = Math.round((1 + Math.random() * 6) * 10) / 10);
renderScoreControls();
gaugeBoard.querySelectorAll('.gauge-card').forEach(c => paintGauge(c, false));
});
document.getElementById('resetBtn').addEventListener('click', () => {
Object.assign(state, defaults);
inspections.forEach(i => scoreState[i.id] = i.score);
document.getElementById('redColor').value = state.red;
document.getElementById('orangeColor').value = state.orange;
document.getElementById('greenColor').value = state.green;
document.getElementById('showTicks').checked = state.showTicks;
document.getElementById('scoreUsesBlackColor').checked = state.scoreUsesBlackColor;
document.getElementById('arcCap').value = state.arcCap;
document.getElementById('animateUpdates').checked = state.animateUpdates;
const rangeMap = [
['arcWidth', 'arcWidth', 'px'], ['scoreSize', 'scoreSize', 'px'], ['scoreY', 'scoreY', ''],
['labelSize', 'labelSize', 'px'], ['labelY', 'labelY', ''], ['inspectionGaugeSize', 'inspectionGaugeSize', 'px'],
['finalGaugeSize', 'finalGaugeSize', 'px'], ['shadowBlur', 'shadowBlur', 'px'], ['shadowY', 'shadowY', 'px'],
['shadowSpread', 'shadowSpread', 'px'], ['animDuration', 'animDuration', 'ms'], ['needleLength', 'needleLength', ''],
['needleBaseWidth', 'needleBaseWidth', 'px'], ['needleTipWidth', 'needleTipWidth', 'px']
];
for (const [id, key, suffix] of rangeMap) {
document.getElementById(id).value = state[key];
const out = document.getElementById(`${id}Out`);
if (out) out.textContent = `${state[key]}${suffix}`;
}
document.getElementById('shadowOpacity').value = state.shadowOpacity;
document.getElementById('shadowOpacityOut').textContent = `${state.shadowOpacity}%`;
renderScoreControls();
syncColors();
repaintAll(true);
});
}
function syncColors(repaint = true) {
document.documentElement.style.setProperty('--red', state.red);
document.documentElement.style.setProperty('--orange', state.orange);
document.documentElement.style.setProperty('--green', state.green);
if (repaint) repaintAll();
}
function repaintAll(firstPaint = false) {
gaugeBoard.querySelectorAll('.gauge-card').forEach(c => paintGauge(c, firstPaint));
}
syncColors(false);
renderGauges();
renderScoreControls();
bindSettings();