Skip to content

Instantly share code, notes, and snippets.

@anvanvan
Last active January 2, 2026 01:44
Show Gist options
  • Select an option

  • Save anvanvan/506ef4cc65d658256aed64a119301458 to your computer and use it in GitHub Desktop.

Select an option

Save anvanvan/506ef4cc65d658256aed64a119301458 to your computer and use it in GitHub Desktop.
Life Calendar Wallpaper Generator for Scriptable iOS app
// Life Calendar Wallpaper Generator for Scriptable
// https://waitbutwhy.com/2014/05/life-weeks.html
// https://x.com/luismbat/status/2006693230109176288
// https://x.com/pastabrian/status/2006756624246112619
// https://x.com/anvanvan/status/2006887614671106311
// Install instructions:
// 1. Download https://scriptable.app
// 2. Go to Shortcut app and create a new shortcut
// 3. Add `Run Inline Script` and `Set Wallpaper`
// 4. Paste this file's content into the `Run Inline Script` step
// 5. In automation, set a daily automation to run the shortcut above at midnight
// Adjust your Birthday and iPhone/iPad screen sizes below:
// Input: { birthday: "YYYY-MM-DD", width: 1290, height: 2796 }
// iPhone 17 Pro Max / 16 Pro Max: 1320 x 2868
// iPhone 16 Plus / 15 Pro Max / 15 Plus: 1290 x 2796
// iPhone Air: 1260 x 2736
// iPhone 17 / 17 Pro / 16 Pro: 1206 x 2622
// iPhone 16 / 15 / 15 Pro: 1179 x 2556
// iPhone 16e: 1170 x 2532
// iPad Pro 13" M4: 2064 x 2752
// iPad Pro 11" M4: 1668 x 2420
// iPad Air 13" M2: 2048 x 2732
// iPad Air 11" M2 / iPad 10th-11th: 1640 x 2360
// iPad mini 7th: 1488 x 2266
const params = args.shortcutParameter || {
birthday: "1980-01-01",
width: 1290,
height: 2796
};
const birthday = new Date(params.birthday);
const width = params.width;
const height = params.height;
const lifeExpectancy = 80;
const weeksPerYear = 52;
// Calculate lived weeks based on age years + weeks since last birthday
const now = new Date();
const msPerWeek = 7 * 24 * 60 * 60 * 1000;
const birthYear = birthday.getFullYear();
const birthMonth = birthday.getMonth();
const birthDay = birthday.getDate();
// Calculate age in years
let ageYears = now.getFullYear() - birthYear;
const birthdayThisYear = new Date(now.getFullYear(), birthMonth, birthDay);
if (now < birthdayThisYear) {
ageYears--;
}
// Find most recent birthday
const lastBirthday = new Date(now.getFullYear(), birthMonth, birthDay);
if (now < lastBirthday) {
lastBirthday.setFullYear(lastBirthday.getFullYear() - 1);
}
// Weeks since last birthday
const weeksSinceBirthday = Math.floor((now - lastBirthday) / msPerWeek);
// Total weeks lived (grid position)
const livedWeeks = ageYears * weeksPerYear + weeksSinceBirthday + 1;
// Grid configuration
const topPadding = Math.round(height * 0.25);
const sidePadding = Math.round(width * 0.06);
const bottomPadding = Math.round(height * 0.28);
const labelHeight = 60;
const gridTop = topPadding + labelHeight;
const gridWidth = width - (sidePadding * 2);
const gridHeight = height - gridTop - bottomPadding;
const dotSpacingX = gridWidth / weeksPerYear;
const dotSpacingY = gridHeight / lifeExpectancy;
const dotRadius = Math.min(dotSpacingX, dotSpacingY) * 0.35;
// Colors
const bgColor = Color.black();
const livedColor = new Color("#ffffff", 1);
const currentColor = new Color("#ff3b30", 1);
const futureColor = new Color("#ffffff", 0.25);
const labelColor = new Color("#ffffff", 0.6);
// Create drawing context
const ctx = new DrawContext();
ctx.size = new Size(width, height);
ctx.opaque = true;
ctx.respectScreenScale = false;
// Fill background
ctx.setFillColor(bgColor);
ctx.fillRect(new Rect(0, 0, width, height));
// Draw labels
ctx.setFont(Font.boldSystemFont(28));
ctx.setTextColor(labelColor);
ctx.setTextAlignedLeft();
ctx.drawText("WEEK OF THE YEAR", new Point(sidePadding, topPadding));
ctx.setTextAlignedRight();
ctx.drawTextInRect("LIFE CALENDAR", new Rect(0, topPadding, width - sidePadding, 40));
// Vertical label - aligned with first row
const verticalText = "YEAR OF YOUR LIFE";
const verticalFontSize = 20;
ctx.setFont(Font.boldSystemFont(verticalFontSize));
ctx.setTextAlignedCenter();
const verticalX = sidePadding * 0.6;
const firstRowY = gridTop + (dotSpacingY / 2);
const verticalStartY = firstRowY - (verticalFontSize / 2);
for (let i = 0; i < verticalText.length; i++) {
ctx.drawText(verticalText[i], new Point(verticalX, verticalStartY + (i * 22)));
}
// Draw grid: 52 columns × 80 rows
let weekCount = 0;
for (let year = 0; year < lifeExpectancy; year++) {
for (let week = 0; week < weeksPerYear; week++) {
const x = sidePadding + (week * dotSpacingX) + (dotSpacingX / 2);
const y = gridTop + (year * dotSpacingY) + (dotSpacingY / 2);
let dotColor;
if (weekCount === livedWeeks - 1) {
dotColor = currentColor;
} else if (weekCount < livedWeeks) {
dotColor = livedColor;
} else {
dotColor = futureColor;
}
ctx.setFillColor(dotColor);
ctx.fillEllipse(new Rect(x - dotRadius, y - dotRadius, dotRadius * 2, dotRadius * 2));
weekCount++;
}
}
// Save to file and return path
const image = ctx.getImage();
const fm = FileManager.local();
const path = fm.temporaryDirectory() + "/life-calendar.png";
fm.writeImage(path, image);
Script.setShortcutOutput(path);
Script.complete();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment