/
home
/
simp499cl
/
public_html
/
notas-prechequeo
/
/home/simp499cl/public_html/notas-prechequeo
mkdir
upload
Name
Size
Mode
Actions
gauges-inspeccion.css
7217
0644
edit
dl
rm
gauges-inspeccion.html
7984
0644
edit
dl
rm
gauges-inspeccion.js
19166
0644
edit
dl
rm
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 ? `<div class="final-rating-label">${finalBand.label}</div>` : ''} <div class="gauge-main"> <div class="gauge-wrap"> <div class="gauge-face"> <svg class="gauge-svg" viewBox="0 0 200 200" role="img" aria-label="${title}: ${fmt(score)} de 7"> <g class="arc-layer"> <path class="arc arc-red" fill="none" /> <path class="arc arc-orange" fill="none" /> <path class="arc arc-green" fill="none" /> <circle class="arc-end-cap arc-cap-start" /> <circle class="arc-end-cap arc-cap-end" /> </g> <g class="tick-mark-layer"></g> <g class="tick-number-layer"></g> <g class="needle-group" data-angle="${ZERO_ANGLE}"> <polygon class="needle" /> <circle class="needle-tip" cx="168" cy="100" r="2" /> <circle class="needle-hub-outer" cx="100" cy="100" r="9" /> <circle class="needle-hub-inner" cx="100" cy="100" r="3.4" /> </g> <text class="gauge-value" x="100" y="155">0.0</text> <rect class="gauge-status-bg" x="64" y="168" width="72" height="20" rx="10" /> <text class="gauge-status" x="100" y="178">${status.label}</text> </svg> </div> </div> <div class="gauge-title">${title}</div> </div>`; 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 = ` <strong>${item.name}</strong> <input type="range" min="1" max="7" step="0.1" value="${scoreState[item.id]}" data-score-id="${item.id}"> <output>${fmt(scoreState[item.id])}</output>`; 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();
Save
cmd:
run