Last active
January 2, 2026 01:44
-
-
Save anvanvan/506ef4cc65d658256aed64a119301458 to your computer and use it in GitHub Desktop.
Life Calendar Wallpaper Generator for Scriptable iOS app
This file contains hidden or 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
| // 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