Paint Calculator
This professional-grade paint calculator helps homeowners, contractors, and facility managers estimate paint quantities for walls, ceilings, and trim—fast and accurately. Enter your room dimensions, openings, coats, and coverage to get exact gallons, a smart can breakdown, and optional cost estimates.
Results
Values update as you type. The totals include coats and the surface factor.
Tip: If you’re between sizes, consider buying a bit extra for touch-ups—especially with dark or highly saturated colors.
Data Source and Methodology
Authoritative Data Source: Benjamin Moore — Paint Calculator & Coverage Guidance (accessed 2025-09-15). See: https://www.benjaminmoore.com/en-us/paint-calculator.
Note: Manufacturers specify coverage on each product label; always defer to the product you intend to use.
Tutti i calcoli si basano rigorosamente sulle formule e sui dati forniti da questa fonte.
The Formula Explained
Wall area per room:
A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra
Ceiling area per room (if included):
A_ceiling = L × W
Trim area per room (optional):
A_trim = \ell_{trim} × w_{trim}
Total equivalent paint area (including coats):
A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t
Effective coverage (adjusted by surface factor s%):
C_eff = C × (s / 100)
Paint required in gallons:
G = A_eq / C_eff
Glossary of Variables
- L, W, H: Room length, width, height.
- n_d, n_w: Number of doors and windows.
- A_d, A_w: Area of a typical door/window (defaults adjustable).
- A_extra: Any additional opening area to subtract (optional per room).
- c_w, c_c, c_t: Coats for walls, ceiling, and trim.
- ℓ_trim, w_trim: Total trim length and trim width.
- C: Label coverage (sq ft/gal or m²/L).
- s: Surface factor percent to account for porosity/texture.
- G: Total paint required in gallons.
How It Works: A Step-by-Step Example
Scenario: 15 ft × 12 ft room, 9 ft walls, 2 doors, 2 windows, include ceiling, no trim. Coverage 350 sq ft/gal. Coats: walls=2, ceiling=1. Surface factor s=100%.
- Compute wall area: 2(L+W)H = 2(15+12)×9 = 486 sq ft.
- Subtract openings: doors 2×21 = 42; windows 2×15 = 30; net walls = 486−42−30 = 414 sq ft.
- Ceiling area: L×W = 15×12 = 180 sq ft.
- Equivalent area: A_eq = 414×2 + 180×1 = 1008 sq ft-coats.
- Paint: G = 1008 / 350 = 2.88 gal. Recommend 3 gal or optimize with 2 gal + 4 quarts, based on price/availability.
Frequently Asked Questions (FAQ)
Do I need primer?
Primer is recommended on new drywall, stained surfaces, drastic color changes, or problem substrates. Primer coverage differs by product; add it as a separate calculation using the primer’s label coverage.
How do I estimate trim paint?
Enter the total linear length of baseboards/casings and set a typical width (e.g., 3.5 in). The tool converts linear length to area and applies trim coats.
What if my room isn’t perfectly rectangular?
Split it into multiple rooms or use the “Extra opening area” to subtract large features. For complex shapes, approximate by sections and add multiple rooms.
How precise are the can recommendations?
We search combinations of 5-gal, 1-gal, and quart cans to meet or slightly exceed the need with minimal waste. If prices are provided, we also minimize estimated cost.
Will rough surfaces affect the estimate?
Yes. Increase the surface factor from 100% to 110–130% for rough or porous surfaces; the calculator lowers effective coverage accordingly.
Can I save or print the results?
Use your browser’s print or export to PDF. We maintain a clear, printer-friendly layout.
Formula (LaTeX) + variables + units
','
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (LaTeX) + variables + units
','
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (LaTeX) + variables + units
','
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (LaTeX) + variables + units
','
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (LaTeX) + variables + units
','
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (LaTeX) + variables + units
','
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (LaTeX) + variables + units
','
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (LaTeX) + variables + units
','
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (LaTeX) + variables + units
','
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
- No variables provided in audit spec.
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy - https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Last code update: 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `\]
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Formula (extracted text)
\[\]
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
Variables and units
- No variables provided in audit spec.
Sources (authoritative):
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy
- https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Changelog
Version: 0.1.0-draft
Last code update: 2026-01-19
0.1.0-draft · 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
amp;') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
- No variables provided in audit spec.
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy - https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Last code update: 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `\]
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Formula (extracted text)
\[\]
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
Variables and units
- No variables provided in audit spec.
Sources (authoritative):
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy
- https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Changelog
Version: 0.1.0-draft
Last code update: 2026-01-19
0.1.0-draft · 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
amp;') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
- No variables provided in audit spec.
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy - https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Last code update: 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `\]
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Formula (extracted text)
\[\]
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
Variables and units
- No variables provided in audit spec.
Sources (authoritative):
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy
- https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Changelog
Version: 0.1.0-draft
Last code update: 2026-01-19
0.1.0-draft · 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
amp;') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
- No variables provided in audit spec.
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy - https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Last code update: 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `\]
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Formula (extracted text)
\[\]
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
Variables and units
- No variables provided in audit spec.
Sources (authoritative):
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy
- https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Changelog
Version: 0.1.0-draft
Last code update: 2026-01-19
0.1.0-draft · 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
amp;') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
- No variables provided in audit spec.
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy - https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Last code update: 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Audit: Complete
Formula (LaTeX) + variables + units
This section shows the formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `\]
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Formula (extracted text)
\[\]
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
Variables and units
- No variables provided in audit spec.
Sources (authoritative):
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy
- https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Changelog
Version: 0.1.0-draft
Last code update: 2026-01-19
0.1.0-draft · 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
amp;') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
- No variables provided in audit spec.
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy - https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Last code update: 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `\]
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Formula (extracted text)
\[\]
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
Variables and units
- No variables provided in audit spec.
Sources (authoritative):
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy
- https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Changelog
Version: 0.1.0-draft
Last code update: 2026-01-19
0.1.0-draft · 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
amp;') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
- No variables provided in audit spec.
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy - https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Last code update: 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `\]
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Formula (extracted text)
\[\]
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
Variables and units
- No variables provided in audit spec.
Sources (authoritative):
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy
- https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Changelog
Version: 0.1.0-draft
Last code update: 2026-01-19
0.1.0-draft · 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
amp;') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
- No variables provided in audit spec.
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy - https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Last code update: 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'
Formula (LaTeX) + variables + units
This section shows the exact formulas used by the calculator engine, plus variable definitions and units.
Formula (extracted LaTeX)
\[','\]
','
Formula (extracted LaTeX)
\[= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =\]
= (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const clamp = (v,min,max) => Math.max(min, Math.min(max, v)); const toNumber = v => (typeof v === 'number') ? v : parseFloat(String(v).replace(/,/g,'')) || 0; const round2 = v => Math.round(v*100)/100; // State const state = { unit: 'imperial', // 'imperial' or 'metric' rooms: [], nextRoomId: 1 }; // Elements const roomsContainer = $('#roomsContainer'); const addRoomBtn = $('#addRoomBtn'); const coverageInput = $('#coverageInput'); const porosityInput = $('#porosity'); const wallCoatsInput = $('#wallCoats'); const ceilingCoatsInput = $('#ceilingCoats'); const trimCoatsInput = $('#trimCoats'); const doorAreaInput = $('#doorArea'); const windowAreaInput = $('#windowArea'); const trimWidthInput = $('#trimWidth'); const calculateBtn = $('#calculateBtn'); const resetBtn = $('#resetBtn'); const priceQuart = $('#priceQuart'); const priceGallon = $('#priceGallon'); const priceFive = $('#priceFiveGallon'); // Results const totalAreaEqEl = $('#totalAreaEq'); const totalGallonsEl = $('#totalGallons'); const effectiveCoverageEl = $('#effectiveCoverage'); const wallsBreakdownEl = $('#wallsBreakdown'); const ceilingsBreakdownEl = $('#ceilingsBreakdown'); const trimBreakdownEl = $('#trimBreakdown'); const canRecommendationEl = $('#canRecommendation'); const costEstimateEl = $('#costEstimate'); const unitRadios =
Formula (extracted LaTeX)
\[('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `\]
('.toggle-help').forEach(btn=>{ btn.addEventListener('click', (e)=>{ const id = btn.getAttribute('aria-controls'); const tip = document.getElementById(id); const expanded = btn.getAttribute('aria-expanded') === 'true'; btn.setAttribute('aria-expanded', String(!expanded)); tip.setAttribute('aria-hidden', String(expanded)); }); btn.addEventListener('keydown', (e)=>{ if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); btn.click(); } }); }); // Create Room UI function createRoom(){ const id = state.nextRoomId++; const room = { id, name: `Room ${id}`, length: 12, width: 12, height: 8, doors: 1, windows: 1, includeCeiling: true, trimLength: 0, extraOpening: 0, collapsed: false }; state.rooms.push(room); renderRooms(); return room; } function renderRooms(){ roomsContainer.innerHTML = ''; state.rooms.forEach(room=>{ const roomEl = document.createElement('div'); roomEl.className = 'room'; roomEl.setAttribute('data-id', room.id); roomEl.innerHTML = ` <div class="room-header"> <div class="room-title" aria-live="polite">${escapeHtml(room.name)}</div> <div class="room-actions"> <button type="button" class="btn collapse-btn" aria-expanded="${!room.collapsed}" aria-controls="room-${room.id}-body">${room.collapsed ? 'Expand' : 'Collapse'}</button> <button type="button" class="btn remove-btn" aria-label="Remove ${escapeHtml(room.name)}">Remove</button> </div> </div> <div id="room-${room.id}-body" ${room.collapsed ? 'hidden' : ''}> <div class="form-group"> <label for="room-name-${room.id}">Room name</label> <input id="room-name-${room.id}" class="form-control" type="text" value="${escapeHtml(room.name)}"> </div> <div class="grid grid-3"> <div class="form-group"> <label for="len-${room.id}">Length<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="len-${room.id}" class="form-control room-field" data-field="length" type="number" min="0.1" step="0.01" value="${room.length}" aria-required="true" aria-describedby="len-err-${room.id} len-suf-${room.id}"> <span id="len-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="len-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="wid-${room.id}">Width<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="wid-${room.id}" class="form-control room-field" data-field="width" type="number" min="0.1" step="0.01" value="${room.width}" aria-required="true" aria-describedby="wid-err-${room.id} wid-suf-${room.id}"> <span id="wid-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="wid-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="hei-${room.id}">Height<span class="required" aria-hidden="true">*</span></label> <div class="input-wrap"> <input id="hei-${room.id}" class="form-control room-field" data-field="height" type="number" min="0.1" step="0.01" value="${room.height}" aria-required="true" aria-describedby="hei-err-${room.id} hei-suf-${room.id}"> <span id="hei-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> <div id="hei-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="doors-${room.id}">Doors</label> <input id="doors-${room.id}" class="form-control room-field" data-field="doors" type="number" min="0" step="1" value="${room.doors}" aria-describedby="doors-err-${room.id}"> <div id="doors-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="windows-${room.id}">Windows</label> <input id="windows-${room.id}" class="form-control room-field" data-field="windows" type="number" min="0" step="1" value="${room.windows}" aria-describedby="windows-err-${room.id}"> <div id="windows-err-${room.id}" class="error-text" role="alert" aria-live="polite"></div> </div> <div class="form-group"> <label for="extra-${room.id}">Extra opening area</label> <div class="input-wrap"> <input id="extra-${room.id}" class="form-control room-field" data-field="extraOpening" type="number" min="0" step="0.1" value="${room.extraOpening}" aria-describedby="extra-suf-${room.id}"> <span id="extra-suf-${room.id}" class="suffix">${state.unit === 'imperial' ? 'sq ft' : 'm²'}</span> </div> </div> </div> <div class="grid grid-3"> <div class="form-group"> <label for="trim-${room.id}">Trim length</label> <div class="input-wrap"> <input id="trim-${room.id}" class="form-control room-field" data-field="trimLength" type="number" min="0" step="0.1" value="${room.trimLength}"> <span class="suffix">${state.unit === 'imperial' ? 'ft' : 'm'}</span> </div> </div> <div class="form-group"> <label for="ceiling-${room.id}">Include ceiling</label> <div class="radio-chip-group"> <div class="radio-chip"> <input type="radio" id="ceiling-yes-${room.id}" name="ceiling-${room.id}" value="yes" ${room.includeCeiling ? 'checked' : ''}> <label for="ceiling-yes-${room.id}">Yes</label> </div> <div class="radio-chip"> <input type="radio" id="ceiling-no-${room.id}" name="ceiling-${room.id}" value="no" ${room.includeCeiling ? '' : 'checked'}> <label for="ceiling-no-${room.id}">No</label> </div> </div> </div> </div> </div> `; // Events roomEl.querySelector('.collapse-btn').addEventListener('click', ()=>{ room.collapsed = !room.collapsed; renderRooms(); }); roomEl.querySelector('.remove-btn').addEventListener('click', ()=>{ state.rooms = state.rooms.filter(r=>r.id !== room.id); renderRooms(); computeAndRender(); }); roomEl.querySelector(`#room-name-${room.id}`).addEventListener('input', (e)=>{ room.name = e.target.value || `Room ${room.id}`; roomEl.querySelector('.room-title').textContent = room.name; }); // Room field handling roomEl.querySelectorAll('.room-field').forEach(inp=>{ inp.addEventListener('blur', ()=> validateField(inp)); inp.addEventListener('input', ()=>{ const f = inp.getAttribute('data-field'); let val = toNumber(inp.value); if(['length','width','height'].includes(f) && val <= 0){ /* validation later */ } if(['doors','windows'].includes(f)){ val = Math.max(0, Math.floor(val)); inp.value = val; } room[f] = val; computeAndRenderDebounced(); }); }); // Ceiling toggle roomEl.querySelector(`#ceiling-yes-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = true; computeAndRenderDebounced(); }); roomEl.querySelector(`#ceiling-no-${room.id}`).addEventListener('change', (e)=>{ room.includeCeiling = false; computeAndRenderDebounced(); }); roomsContainer.appendChild(roomEl); }); } function validateField(input){ const val = toNumber(input.value); const id = input.id; const errEl = document.getElementById(id.replace(/^(.+?)(?=-|$)/,'$&') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Formula (extracted text)
\[\]
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
Variables and units
- No variables provided in audit spec.
Sources (authoritative):
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy
- https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Changelog
Version: 0.1.0-draft
Last code update: 2026-01-19
0.1.0-draft · 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.
amp;') + '-err-' + (id.split('-').pop())); // For global fields we may not have the same pattern; skip error mapping if not found let errorTarget = null; if (!errEl && input.getAttribute('aria-describedby')){ const ids = input.getAttribute('aria-describedby').split(' '); errorTarget = ids.map(i=>document.getElementById(i)).find(el=>el && el.classList.contains('error-text')); } else { errorTarget = errEl; } let msg = ''; if(input.hasAttribute('aria-required') && (isNaN(val) || val <= 0)){ msg = 'Please enter a value greater than 0.'; } if(input.id.startsWith('doors') || input.id.startsWith('windows')){ if(val < 0 || !Number.isFinite(val)) msg = 'Please enter a non-negative whole number.'; } input.setAttribute('aria-invalid', msg ? 'true' : 'false'); if(errorTarget){ errorTarget.textContent = msg; } } // Unit handling unitRadios.forEach(r=>{ r.addEventListener('change', ()=>{ state.unit = r.value; updateUnitSuffixes(); computeAndRender(); }); }); function updateUnitSuffixes(){ if(state.unit === 'imperial'){ suffixCoverage.textContent = 'sq ft / gallon'; suffixDoor.textContent = 'sq ft'; suffixWindow.textContent = 'sq ft'; suffixTrim.textContent = 'inches'; // Update room field suffixes dynamically }else{ suffixCoverage.textContent = 'm² / liter'; suffixDoor.textContent = 'm²'; suffixWindow.textContent = 'm²'; suffixTrim.textContent = 'cm'; } // Re-render rooms to update inline suffixes renderRooms(); } // Coverage conversion // Returns effective coverage in sq ft per gallon function getEffectiveCoverageSqFtPerGallon(){ const cov = Math.max(1, toNumber(coverageInput.value)); const s = clamp(toNumber(porosityInput.value) || 100, 50, 150); const base = (state.unit === 'imperial') ? cov // sq ft per gallon : cov * 10.76391041671 * 3.785411784; // m²/L -> sq ft/gal const eff = base * (s/100); return eff; } function roomAreasSqFt(room){ // convert inputs to feet const toFeet = (v) => state.unit === 'imperial' ? v : v * 3.280839895; const toSqFt = (v) => state.unit === 'imperial' ? v : v * 10.76391041671; const L = Math.max(0, toFeet(room.length)); const W = Math.max(0, toFeet(room.width)); const H = Math.max(0, toFeet(room.height)); const Adoor = state.unit === 'imperial' ? Math.max(0, toNumber(doorAreaInput.value)) : toSqFt(toNumber(doorAreaInput.value)); const Awin = state.unit === 'imperial' ? Math.max(0, toNumber(windowAreaInput.value)) : toSqFt(toNumber(windowAreaInput.value)); const Aextra = state.unit === 'imperial' ? Math.max(0, toNumber(room.extraOpening)) : toSqFt(Math.max(0, toNumber(room.extraOpening))); let walls = 2*(L+W)*H - Math.max(0, Math.floor(room.doors))*Adoor - Math.max(0, Math.floor(room.windows))*Awin - Aextra; walls = Math.max(0, walls); let ceiling = room.includeCeiling ? (L*W) : 0; // Trim area: length * width // Input: length in ft/m; width in inches/cm defaults global const trimLen = state.unit === 'imperial' ? toNumber(room.trimLength) : toNumber(room.trimLength) * 3.280839895; const trimWidth = state.unit === 'imperial' ? toNumber(trimWidthInput.value)/12 : (toNumber(trimWidthInput.value)/100) * 3.280839895; // convert inches->ft OR cm->m->ft let trim = Math.max(0, trimLen * Math.max(0, trimWidth)); // in square feet already return {walls, ceiling, trim}; } function computeTotals(){ const effCov = getEffectiveCoverageSqFtPerGallon(); // sq ft per gallon const cw = Math.max(0, Math.floor(toNumber(wallCoatsInput.value))); const cc = Math.max(0, Math.floor(toNumber(ceilingCoatsInput.value))); const ct = Math.max(0, Math.floor(toNumber(trimCoatsInput.value))); let wallsArea = 0, ceilingArea = 0, trimArea = 0; state.rooms.forEach(r=>{ const a = roomAreasSqFt(r); wallsArea += a.walls; ceilingArea += a.ceiling; trimArea += a.trim; }); const eqArea = wallsArea*cw + ceilingArea*cc + trimArea*ct; // sq ft-coats const totalGallons = effCov > 0 ? (eqArea / effCov) : 0; // Breakdown per type (gallons) const wallsGal = effCov > 0 ? (wallsArea*cw / effCov) : 0; const ceilGal = effCov > 0 ? (ceilingArea*cc / effCov) : 0; const trimGal = effCov > 0 ? (trimArea*ct / effCov) : 0; return { effCov, eqArea, totalGallons, wallsGal, ceilGal, trimGal, wallsArea, ceilingArea, trimArea, coats: {cw, cc, ct} }; } function gallonsToLiters(gal){ return gal * 3.785411784; } // Can optimization (5, 1, 0.25 gal) function optimizeCans(requiredGallons){ if(requiredGallons <= 0) return {five:0, one:0, quart:0, overage:0}; const maxFive = Math.ceil(requiredGallons / 5); let best = null; for(let f = 0; f <= maxFive; f++){ const remainingAfterFive = Math.max(0, requiredGallons - 5*f); const maxOne = Math.ceil(remainingAfterFive); for(let o = 0; o <= maxOne + 1; o++){ let remaining = Math.max(0, requiredGallons - (5*f + 1*o)); // quarts 0..3 for(let q = 0; q <= 3; q++){ const total = 5*f + 1*o + 0.25*q; if(total < requiredGallons) continue; const over = round2(total - requiredGallons); const cans = f + o + q; const cost = estimateCost({five:f, one:o, quart:q}); const candidate = {five:f, one:o, quart:q, overage:over, cans, cost}; if(!best){ best = candidate; }else{ // Prefer minimal overage, then minimal cost if provided, then fewer cans if(over < best.overage || (over === best.overage && cost != null && best.cost != null && cost < best.cost) || (over === best.overage && (cost == null || best.cost == null) && cans < best.cans) || (over === best.overage && cost != null && best.cost == null) ){ best = candidate; } } } } } return best || {five:0, one:Math.ceil(requiredGallons), quart:0, overage:round2(Math.ceil(requiredGallons)-requiredGallons)}; } function estimateCost(cans){ const pq = toNumber(priceQuart.value); const pg = toNumber(priceGallon.value); const p5 = toNumber(priceFive.value); const hasAny = pq>0 || pg>0 || p5>0; if(!hasAny) return null; const qCost = pq>0 ? pq*cans.quart : 0; const gCost = pg>0 ? pg*cans.one : 0; const fCost = p5>0 ? p5*cans.five : 0; // If some prices missing, assume proportional where needed const fallbackPg = (pg>0) ? pg : (p5>0 ? (p5/5) : (pq>0 ? pq*4 : 0)); const fallbackPq = (pq>0) ? pq : (fallbackPg/4); const fallbackP5 = (p5>0) ? p5 : (fallbackPg*5*0.95); // small bulk discount assumption const cost = (pq>0 ? 0 : fallbackPq*cans.quart) + (pg>0 ? 0 : fallbackPg*cans.one) + (p5>0 ? 0 : fallbackP5*cans.five) + qCost + gCost + fCost; return round2(cost); } // Rendering results function computeAndRender(){ // Validate critical fields [coverageInput, porosityInput, wallCoatsInput, ceilingCoatsInput, trimCoatsInput].forEach(validateField); const totals = computeTotals(); const effCov = round2(totals.effCov); const gallons = round2(totals.totalGallons); const liters = round2(gallonsToLiters(gallons)); effectiveCoverageEl.textContent = isFinite(effCov) ? `${effCov} sq ft/gal` : '—'; totalAreaEqEl.textContent = `${round2(totals.eqArea)} sq ft-coats`; totalGallonsEl.textContent = `${gallons.toFixed(2)} gal (${liters.toFixed(2)} L)`; wallsBreakdownEl.textContent = `${round2(totals.wallsGal).toFixed(2)} gal`; ceilingsBreakdownEl.textContent = `${round2(totals.ceilGal).toFixed(2)} gal`; trimBreakdownEl.textContent = `${round2(totals.trimGal).toFixed(2)} gal`; const rec = optimizeCans(totals.totalGallons); if(rec){ const totalProvided = 5*rec.five + 1*rec.one + 0.25*rec.quart; canRecommendationEl.textContent = `${rec.five}×5 gal, ${rec.one}×1 gal, ${rec.quart}×1 qt (total ${round2(totalProvided)} gal, overage ${rec.overage} gal)`; const cost = estimateCost(rec); costEstimateEl.textContent = cost != null ? `
Wall area per room: A_wall = 2(L + W)H - n_d A_d - n_w A_w - A_extra Ceiling area per room (if included): A_ceiling = L × W Trim area per room (optional): A_trim = \ell_{trim} × w_{trim} Total equivalent paint area (including coats): A_eq = A_wall × c_w + A_ceiling × c_c + A_trim × c_t Effective coverage (adjusted by surface factor s%): C_eff = C × (s / 100) Paint required in gallons: G = A_eq / C_eff
- No variables provided in audit spec.
- Construction — calcdomain.com · Accessed 2026-01-19
https://calcdomain.com/construction-diy - https://www.benjaminmoore.com/en-us/paint-calculator — benjaminmoore.com · Accessed 2026-01-19
https://www.benjaminmoore.com/en-us/paint-calculator
Last code update: 2026-01-19
- Initial audit spec draft generated from HTML extraction (review required).
- Verify formulas match the calculator engine and convert any text-only formulas to LaTeX.
- Confirm sources are authoritative and relevant to the calculator methodology.