Last active
May 9, 2023 03:18
-
-
Save blindside85/cceadbcb91ac5971158823e844df0ba9 to your computer and use it in GitHub Desktop.
I thought it might be fun to build a live/dynamic version of Kurzgesagt's "Calendar of Your Life" (https://shop-us.kurzgesagt.org/collections/bestsellers/products/lifespan-calendar-poster?variant=39451596455984). I couldn't decide if using `canvas` or regular markup + CSS would be more fun / interesting, so I'm doing a bit of both.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Calendar of Life</title> | |
<style> | |
body { | |
padding: 2rem; | |
margin: 0; | |
font: 1.1em/1.3em Helvetica, Arial, sans-serif; | |
color: white; | |
background-color: #222; | |
} | |
main { | |
max-width: 1000px; | |
margin: auto; | |
} | |
.life-math, | |
input { | |
margin-bottom: 1rem; | |
} | |
input { | |
background-color: transparent; | |
color: white; | |
border: none; | |
} | |
canvas { | |
display: block; | |
margin: auto; | |
padding: 1rem; | |
border: 2px dotted white; | |
} | |
</style> | |
</head> | |
<body> | |
<main> | |
<h1>Calendar of Life</h1> | |
<div class="bday"> | |
<label for="bday">Enter your birthday:</label> | |
<input type="date" value="1985-03-02" min="1924-01-01" max="2020-01-01"> | |
</div> | |
<p class="life-math">eyo world</p> | |
<canvas width="1000"></canvas> | |
</main> | |
<script> | |
((win, doc) => { | |
const canvas = doc.querySelector('canvas'); | |
const ctx = canvas.getContext('2d'); | |
const bdayInput = document.querySelector('input'); | |
const dataText = document.querySelector('.life-math'); | |
const dayMillis = 1000 * 60 * 60 * 24; // = 86400000 | |
const yearMillis = dayMillis * 365; | |
const today = new Date(); | |
const currWeekNum = getWeekNum(today); | |
const weeksPerYear = 52; | |
const yearsPerLife = 77; // average life expectancy in the US in 2023 for males | |
const bubbleRadius = canvas.width * 0.008; | |
const bubbleSpacing = canvas.width * 0.002; | |
const bubbleStartX = bubbleRadius * 3; | |
const bubbleStartY = bubbleRadius * 2; | |
const labelX = bubbleStartX + (weeksPerYear + 1) * (bubbleRadius * 2); | |
const stages = [ | |
{ | |
name: 'childhood', | |
years: [0, 12] | |
}, | |
{ | |
name: 'adolescence', | |
years: [13, 19] | |
}, | |
{ | |
name: 'early adulthood', | |
years: [20, 34] | |
}, | |
{ | |
name: 'middle adulthood', | |
years: [35, 49] | |
}, | |
{ | |
name: 'mature adulthood', | |
years: [50, 79] | |
}, | |
{ | |
name: 'late adulthood', | |
years: [80, 100] | |
} | |
]; | |
let bday = bdayInput.valueAsDate; | |
let currAge = Math.floor((today - bday) / yearMillis); | |
let bdayWeekNum = getWeekNum(bday); | |
bdayInput.addEventListener('change', evt => { | |
bday = evt.target.valueAsDate; | |
currAge = Math.floor((today - bday) / yearMillis); | |
bdayWeekNum = getWeekNum(bday); | |
setDataText(); | |
renderCal(); | |
}); | |
function getWeekNum(day) { | |
const firstDayOfYear = new Date(day.getFullYear(), 0, 4); | |
const daysSinceFirstDayOfYear = (day - firstDayOfYear) / dayMillis; | |
const daysSinceFirstDayOfWeek = daysSinceFirstDayOfYear + firstDayOfYear.getDay() + 1; | |
return Math.ceil(daysSinceFirstDayOfWeek / 7); | |
}; | |
function setDataText() { | |
const lifeWeeks = weeksPerYear * currAge - bdayWeekNum; | |
const totalWeeks = weeksPerYear * yearsPerLife; | |
const lifePercentage = (lifeWeeks/totalWeeks*100).toPrecision(3); | |
dataText.innerHTML = `You've lived ${lifeWeeks.toLocaleString()} weeks (${lifePercentage}%) of the ${totalWeeks.toLocaleString()} possible weeks of an average ${yearsPerLife}-year human life.`; | |
} | |
function fillCircle(midX, midY, radius, strokeWidth, fillColor, strokeColor) { | |
ctx.beginPath(); | |
ctx.arc(midX, midY, radius, 0, 2 * Math.PI, false); | |
ctx.fillStyle = fillColor; | |
ctx.fill(); | |
ctx.lineWidth = strokeWidth; | |
ctx.strokeStyle = strokeColor; | |
ctx.stroke(); | |
} | |
function renderCal() { | |
ctx.fillStyle = 'white'; | |
ctx.font = '2em Arial'; | |
for (const [idx, stage] of stages.entries()) { | |
// auto-skip stages that are outside of the life span | |
if (stage.years[0] >= yearsPerLife) { | |
return; | |
} | |
const labelY = bubbleStartY + stage.years[0] * (bubbleRadius * 2.5); | |
const stageColor = `hsl(${(idx + 1) * Math.ceil(Math.random() * Date.now() / 100000)}, 40%, 50%)`; | |
ctx.fillStyle = 'white'; | |
ctx.font = '0.8em Arial'; | |
ctx.textAlign = 'left'; | |
// position and print life stage labels | |
ctx.save(); | |
ctx.translate(labelX, labelY - bubbleSpacing); | |
ctx.rotate(Math.PI / 2); | |
ctx.fillText(`${stage.name}`, 0, 0); | |
ctx.restore(); | |
// handle individual years | |
for (let year = stage.years[0]; year <= stage.years[1] && year <= yearsPerLife; year++) { | |
const rowY = bubbleStartY + year * (bubbleRadius * 2.5); | |
for (let week = 1; week <= weeksPerYear; week++) { | |
if (year === 0 && week > bdayWeekNum || year < currAge && year > 0 || year === currAge && week < currWeekNum) { | |
// fill in bubbles of weeks lived | |
fillCircle(bubbleStartX + week * (bubbleRadius * 2), rowY, bubbleRadius - bubbleSpacing, 1, stageColor, stageColor); | |
} else { | |
// empty bubbles for weeks not lived | |
fillCircle(bubbleStartX + week * (bubbleRadius * 2), rowY, bubbleRadius - bubbleSpacing, 1, 'transparent', stageColor); | |
} | |
} | |
// position and print year labels | |
ctx.fillStyle = 'white'; | |
ctx.font = '0.8em Arial'; | |
ctx.textAlign = 'right'; | |
ctx.fillText(year, bubbleStartX, rowY + bubbleSpacing * 2); | |
} | |
} | |
} | |
canvas.height = (bubbleRadius * 2 + bubbleSpacing * 3) * yearsPerLife; | |
setDataText(); | |
renderCal(); | |
})(window, document); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment