(function() { 'use strict'; var form = document.getElementById('weekends-form'); var birthYearInput = document.getElementById('birth-year'); var deathAgeInput = document.getElementById('death-age'); var resultsDiv = document.getElementById('weekends-results'); var percentageRemainingEl = document.getElementById('percentage-remaining'); var usedCountEl = document.getElementById('used-count'); var totalCountEl = document.getElementById('total-count'); var wakingHoursEl = document.getElementById('waking-hours'); var gridEl = document.getElementById('weekends-grid'); var downloadBtn = document.getElementById('download-btn'); var lastCalculation = null; function countWeekends(startDate, endDate) { var count = 0; var current = new Date(startDate); current.setHours(0, 0, 0, 0); while (current <= endDate) { if (current.getDay() === 6) { count++; } current.setDate(current.getDate() + 1); } return count; } function formatNumber(num) { return num.toLocaleString(); } function renderGrid(usedCount, remainingCount, birthYear) { gridEl.innerHTML = ''; var existingMilestones = document.querySelector('.grid-milestones'); if (existingMilestones) { existingMilestones.remove(); } var total = usedCount + remainingCount; var maxCells = 5000; var scaled = total > maxCells; var currentCellIndex; var cellCount; if (scaled) { var scaleFactor = maxCells / total; var usedToRender = Math.round(usedCount * scaleFactor); var remainingToRender = maxCells - usedToRender; currentCellIndex = usedToRender > 0 ? usedToRender - 1 : 0; cellCount = maxCells; for (var i = 0; i < usedToRender; i++) { var cell = document.createElement('div'); cell.className = 'grid-cell used'; if (i === currentCellIndex) { cell.className += ' current'; } gridEl.appendChild(cell); } for (var j = 0; j < remainingToRender; j++) { var cell = document.createElement('div'); cell.className = 'grid-cell'; gridEl.appendChild(cell); } var overflow = document.createElement('div'); overflow.className = 'grid-overflow'; overflow.textContent = '(' + formatNumber(total) + ' total weekends)'; gridEl.appendChild(overflow); } else { currentCellIndex = usedCount > 0 ? usedCount - 1 : 0; cellCount = total; for (var i = 0; i < usedCount; i++) { var cell = document.createElement('div'); cell.className = 'grid-cell used'; if (i === currentCellIndex) { cell.className += ' current'; } gridEl.appendChild(cell); } for (var j = 0; j < remainingCount; j++) { var cell = document.createElement('div'); cell.className = 'grid-cell'; gridEl.appendChild(cell); } } var milestones = [20, 40, 60, 80]; var milestoneText = milestones.map(function(age) { return 'Age ' + age + ': ' + (birthYear + age); }).join(' · '); var milestonesEl = document.createElement('div'); milestonesEl.className = 'grid-milestones'; milestonesEl.textContent = milestoneText; gridEl.parentNode.appendChild(milestonesEl); } function generateImage() { if (!lastCalculation) return; var fontUrl = '/fonts/JetBrainsMonoNLNerdFont-Regular.ttf'; var font = new FontFace('JetBrainsMono', 'url(' + fontUrl + ')'); font.load().then(function(loadedFont) { document.fonts.add(loadedFont); renderImage(); }).catch(function() { renderImage(); }); function renderImage() { var data = lastCalculation; var canvasWidth = 800; var cellSize = 6; var cellGap = 2; var padding = 30; var headerHeight = 100; var footerHeight = 60; var total = data.usedCount + data.remainingCount; var maxCells = 5000; var scaled = total > maxCells; var cellCount = scaled ? maxCells : total; var cellsPerRow = Math.floor((canvasWidth - 2 * padding) / (cellSize + cellGap)); var rows = Math.ceil(cellCount / cellsPerRow); var gridHeight = rows * (cellSize + cellGap); var canvasHeight = headerHeight + gridHeight + footerHeight + padding; var canvas = document.createElement('canvas'); canvas.width = canvasWidth; canvas.height = canvasHeight; var ctx = canvas.getContext('2d'); var fontFamily = 'JetBrainsMono, monospace'; var bgColor = '#131a21'; var textColor = getComputedStyle(document.documentElement).getPropertyValue('--text-color').trim() || '#333333'; var primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#007bff'; ctx.fillStyle = bgColor; ctx.fillRect(0, 0, canvasWidth, canvasHeight); ctx.fillStyle = primaryColor; ctx.font = 'bold 36px ' + fontFamily; ctx.fillText(data.percentageRemaining + '% weekends remaining', padding, padding + 30); ctx.fillStyle = textColor; ctx.font = '14px ' + fontFamily; var summary = formatNumber(data.usedCount) + ' / ' + formatNumber(data.totalCount) + ' weekends · ' + formatNumber(data.wakingHours) + ' waking hours remaining (assuming 8h sleep on saturdays)'; ctx.fillText(summary, padding, padding + 55); var scaleFactor = scaled ? maxCells / total : 1; var usedToRender = scaled ? Math.round(data.usedCount * scaleFactor) : data.usedCount; var currentCellIndex = usedToRender > 0 ? usedToRender - 1 : 0; for (var i = 0; i < cellCount; i++) { var col = i % cellsPerRow; var row = Math.floor(i / cellsPerRow); var x = padding + col * (cellSize + cellGap); var y = headerHeight + row * (cellSize + cellGap); var isUsed = i < usedToRender; ctx.fillStyle = primaryColor; ctx.globalAlpha = isUsed ? 0.3 : 1; ctx.beginPath(); ctx.roundRect(x, y, cellSize, cellSize, 1); ctx.fill(); ctx.globalAlpha = 1; if (i === currentCellIndex) { ctx.strokeStyle = textColor; ctx.lineWidth = 2; ctx.beginPath(); ctx.roundRect(x - 2, y - 2, cellSize + 4, cellSize + 4, 2); ctx.stroke(); } } ctx.fillStyle = textColor; ctx.font = '20px ' + fontFamily; var subtitle = 'posixlycorrect.com/lib/weekends-left'; ctx.fillText(subtitle, padding, canvasHeight - 20); var link = document.createElement('a'); link.download = 'weekends-left.png'; link.href = canvas.toDataURL('image/png'); link.click(); } } function calculate() { var birthYear = parseInt(birthYearInput.value, 10); var deathAge = parseInt(deathAgeInput.value, 10); if (!birthYear || !deathAge) { resultsDiv.style.display = 'none'; return; } if (deathAge < 1 || deathAge > 999) { resultsDiv.style.display = 'none'; return; } var deathYear = birthYear + deathAge; var birthDate = new Date(birthYear, 0, 1); var deathDate = new Date(deathYear, 11, 31); var today = new Date(); today.setHours(0, 0, 0, 0); var usedWeekends = countWeekends(birthDate, today); var remainingWeekends = countWeekends(today, deathDate); var totalWeekends = usedWeekends + remainingWeekends; var percentage = Math.round((usedWeekends / totalWeekends) * 100); var wakingHours = remainingWeekends * 40; var percentageRemaining = 100 - percentage; lastCalculation = { usedCount: usedWeekends, remainingCount: remainingWeekends, totalCount: totalWeekends, percentage: percentage, percentageRemaining: percentageRemaining, wakingHours: wakingHours, birthDate: birthDate }; percentageRemainingEl.textContent = percentageRemaining + '%'; usedCountEl.textContent = formatNumber(usedWeekends); totalCountEl.textContent = formatNumber(totalWeekends); wakingHoursEl.textContent = formatNumber(wakingHours) + ' waking hours remaining, assuming 8h of sleep on saturdays'; renderGrid(usedWeekends, remainingWeekends, birthYear); resultsDiv.style.display = 'block'; } birthYearInput.addEventListener('input', calculate); deathAgeInput.addEventListener('input', calculate); form.addEventListener('submit', function(e) { e.preventDefault(); calculate(); }); downloadBtn.addEventListener('click', generateImage); })();