Skip to content

Instantly share code, notes, and snippets.

@blindside85
Last active May 9, 2023 03:18
Show Gist options
  • Save blindside85/cceadbcb91ac5971158823e844df0ba9 to your computer and use it in GitHub Desktop.
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.
<!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