🌈️ apply a wallpaper based on the weather outside
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: use CIEDE2000 algo to calculate colour difference instead

also map lightness to sqrt of radiation because that is how the sky
works

Angel Wang (Jun 22, 2026, 11:36 AM -0600) c9903be1 72dbbe1f

+55 -21
+20 -13
apply/application.ts
··· 1 1 import type { ImageAnalysisData } from "../analyze/analysis.ts" 2 + import * as colourDiff from "color-diff" 2 3 3 4 export interface ApplicationConfig { 4 5 latitude: number ··· 77 78 chromaValue: number, 78 79 lightnessValue: number, 79 80 ) { 80 - const targetColour = [lightnessValue, chromaValue, hueValue] 81 - let closestColour = imagesData.files[0] 81 + // let's convert Oklch to CIELAB first: 82 + const aValue = chromaValue * Math.cos(hueValue) 83 + const bValue = chromaValue * Math.sin(hueValue) 82 84 83 - for (const file of imagesData.files) { 84 - if ( 85 - Math.abs(file.oklch[0] - targetColour[0]) <= Math.abs(closestColour.oklch[0] - targetColour[0]) && 86 - Math.abs(file.oklch[1] - targetColour[1]) <= Math.abs(closestColour.oklch[1] - targetColour[1]) && 87 - Math.abs(file.oklch[2] - targetColour[2]) <= Math.abs(closestColour.oklch[2] - targetColour[2]) 88 - ) { 89 - closestColour = file 90 - } 91 - } 92 - console.info(`Found target image ${closestColour.path} with colour oklch(${closestColour.oklch[0]} ${closestColour.oklch[1]} ${closestColour.oklch[2]})!`) 85 + const matchingImages = [] 93 86 94 - return closestColour 87 + let targetDifference = 0.07 88 + while (matchingImages.length == 0) { 89 + for (const image of imagesData.files) { 90 + const targetAValue = image.oklch[1] * Math.cos(image.oklch[2]) 91 + const targetBValue = image.oklch[1] * Math.sin(image.oklch[2]) 92 + 93 + // then use the CIEDE2000 algorithm to calculate the difference between the colours 94 + const difference = colourDiff.diff({L: lightnessValue, a: aValue, b: bValue}, {L: image.oklch[0], a: targetAValue, b: targetBValue}) 95 + if (difference <= targetDifference) { 96 + matchingImages.push(image) 97 + } 98 + } 99 + targetDifference += 0.01 100 + } 101 + return matchingImages 95 102 }
+8 -6
apply/index.ts
··· 1 1 #!/usr/bin/env zx 2 2 3 - import { ImageAnalysisData } from "../analyze/analysis.ts" 4 - import { cacheDir, chalkDebug, configDir, loadConfig, map, mapEaseOutQuint } from "../utils.ts" 5 - import { ApplicationConfig, defaultConfig, findMatchingImages, getOpenMeteoData } from "./application.ts" 3 + import type { ImageAnalysisData } from "../analyze/analysis.ts" 4 + import { cacheDir, chalkDebug, configDir, loadConfig, map, mapEaseOutQuint, mapEaseOutSqrt } from "../utils.ts" 5 + import { type ApplicationConfig, defaultConfig, findMatchingImages, getOpenMeteoData } from "./application.ts" 6 6 import * as SunCalc from "suncalc" 7 7 import "zx/globals" 8 8 ··· 57 57 config.chromaRange.end, 58 58 ) 59 59 60 - const lightnessValue = map( 60 + const lightnessValue = mapEaseOutSqrt( 61 61 Number(openMeteoData.shortwaveRadiation), 62 62 0, 63 63 1000, ··· 74 74 const imagesData = JSON.parse(cacheFile) as ImageAnalysisData 75 75 console.debug(chalkDebug(`Opened cache file ${pathToCache}!`)) 76 76 77 - const targetFile = findMatchingImages(imagesData, hueValue, chromaValue, lightnessValue) 77 + const targetImages = findMatchingImages(imagesData, hueValue, chromaValue, lightnessValue) 78 + const targetImage = targetImages[Math.floor(Math.random() * targetImages.length)] 79 + console.info(`Found target image ${targetImage.path} with colour oklch(${targetImage.oklch[0]} ${targetImage.oklch[1]} ${targetImage.oklch[2]})!`) 78 80 79 81 console.info(`Setting wallpaper...`) 80 - await $`eval ${config.applyWallpaperCommand.replace("%s", targetFile.path)}` 82 + await $`eval ${config.applyWallpaperCommand.replace("%s", targetImage.path)}` 81 83 console.info(`Success! Enjoy :)`)
+2 -2
deno.json
··· 9 9 "tasks": { 10 10 "compile-analyze": "deno compile --output build/rainwall-analyze -A analyze/index.ts", 11 11 "compile-apply": "deno compile --output build/rainwall-apply -A apply/index.ts", 12 - "analyze": "deno run -A analyze/index.ts", 13 - "apply": "deno run -A apply/index.ts" 12 + "analyze": "deno -A analyze/index.ts", 13 + "apply": "deno -A apply/index.ts" 14 14 }, 15 15 "imports": { 16 16 "@std/assert": "jsr:@std/assert@1",
+9
deno.lock
··· 5 5 "jsr:@std/internal@^1.0.12": "1.0.14", 6 6 "jsr:@std/internal@^1.0.14": "1.0.14", 7 7 "jsr:@std/path@^1.1.5": "1.1.5", 8 + "npm:chalk@^5.6.2": "5.6.2", 9 + "npm:color-diff@^1.4.0": "1.4.0", 8 10 "npm:suncalc@2": "2.0.0", 9 11 "npm:zx@^8.8.5": "8.8.5" 10 12 }, ··· 26 28 } 27 29 }, 28 30 "npm": { 31 + "chalk@5.6.2": { 32 + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==" 33 + }, 34 + "color-diff@1.4.0": { 35 + "integrity": "sha512-4oDB/o78lNdppbaqrg0HjOp7pHmUc+dfCxWKWFnQg6AB/1dkjtBDop3RZht5386cq9xBUDRvDvSCA7WUlM9Jqw==" 36 + }, 29 37 "suncalc@2.0.0": { 30 38 "integrity": "sha512-RcO28GOMXNnB+sr+c7/+zv5UPMa9wxIsUVGE3HddjQykqCVEC2rc4t4IjBB7qaa7K7dx5Cxp8UZwexyXrX8JkA==" 31 39 }, ··· 42 50 "packageJson": { 43 51 "dependencies": [ 44 52 "npm:chalk@^5.6.2", 53 + "npm:color-diff@^1.4.0", 45 54 "npm:suncalc@2", 46 55 "npm:zx@^8.8.5" 47 56 ]
+7
package-lock.json
··· 6 6 "": { 7 7 "dependencies": { 8 8 "chalk": "^5.6.2", 9 + "color-diff": "^1.4.0", 9 10 "suncalc": "^2.0.0", 10 11 "zx": "^8.8.5" 11 12 } ··· 21 22 "funding": { 22 23 "url": "https://github.com/chalk/chalk?sponsor=1" 23 24 } 25 + }, 26 + "node_modules/color-diff": { 27 + "version": "1.4.0", 28 + "resolved": "https://registry.npmjs.org/color-diff/-/color-diff-1.4.0.tgz", 29 + "integrity": "sha512-4oDB/o78lNdppbaqrg0HjOp7pHmUc+dfCxWKWFnQg6AB/1dkjtBDop3RZht5386cq9xBUDRvDvSCA7WUlM9Jqw==", 30 + "license": "BSD-3-Clause" 24 31 }, 25 32 "node_modules/suncalc": { 26 33 "version": "2.0.0",
+1
package.json
··· 1 1 { 2 2 "dependencies": { 3 3 "chalk": "^5.6.2", 4 + "color-diff": "^1.4.0", 4 5 "suncalc": "^2.0.0", 5 6 "zx": "^8.8.5" 6 7 }
+8
utils.ts
··· 88 88 export function mapEaseOutQuint(number: number, min: number, max: number, newMin: number, newMax: number) { 89 89 return easeOutQuint((number - min) / (max - min)) * (newMax - newMin) + newMin 90 90 } 91 + 92 + function easeOutSqrt(x: number): number { 93 + return Math.sqrt(x) 94 + } 95 + 96 + export function mapEaseOutSqrt(number: number, min: number, max: number, newMin: number, newMax: number) { 97 + return easeOutSqrt((number - min) / (max - min)) * (newMax - newMin) + newMin 98 + }