diff --git a/analyze/analysis.mts b/analyze/analysis.mts
index 7aac025..8da6702 100755
--- a/analyze/analysis.mts
+++ b/analyze/analysis.mts
@@ -1,46 +1,19 @@
#!/usr/bin/env zx
-import { floorToStep } from "../utils.mts";
-
///
export interface AnalysisConfig {
imageDir: string
- lightnessStep: number
- chromaStep: number
- hueStep: number
preAnalysisCommands: string[]
}
-export class ImageAnalysisData {
- lightnessData: { lightness: number, paths: string[] }[]
- chromaData: { chroma: number, paths: string[] }[]
- hueData: { hue: number, paths: string[] }[]
-
- constructor(lightnessStep: number, chromaStep: number, hueStep: number) {
- this.lightnessData = []
- this.chromaData = []
- this.hueData = []
-
- for (let i = 0; i < 100; i += lightnessStep) {
- this.lightnessData.push({ lightness: i, paths: [] })
- }
-
- for (let i = 0; i < 100; i += chromaStep) {
- this.chromaData.push({ chroma: i, paths: [] })
- }
- for (let i = 0; i < 360; i += hueStep) {
- this.hueData.push({ hue: i, paths: [] })
- }
- }
+export interface ImageAnalysisData {
+ files: { path: string; oklch: number[] }[]
}
export const defaultConfig: AnalysisConfig = {
imageDir: `${os.homedir()}/Pictures`,
- lightnessStep: 10,
- chromaStep: 1,
- hueStep: 90,
- preAnalysisCommands: []
+ preAnalysisCommands: [],
}
export async function runPreAnalysis(hooks: string[]) {
@@ -49,35 +22,33 @@ export async function runPreAnalysis(hooks: string[]) {
}
}
-export async function analyzeImages(imageDir: string, analysisOutput: ImageAnalysisData, outputPath: string, lightnessStep: number, chromaStep: number, hueStep: number) {
+export async function analyzeImages(imageDir: string, analysisOutput: ImageAnalysisData, outputPath: string) {
const images = await fs.promises.opendir(`${imageDir}`)
for await (const image of images) {
const path = `${imageDir}/${image.name}`
const stat = await fs.promises.stat(path)
if (stat.isFile()) {
- let output =
- await $`magick ${path} -colorspace Oklch -kmeans 10 -format "%c" histogram:info:`.then(
- (s) =>
- s.stdout
- .split("\n")
- .map((substr) => substr.trim())
- .filter((n) => n),
- ) as string[]
+ let output = await $`magick ${path} -colorspace Oklch -kmeans 10 -format "%c" histogram:info:`.then(
+ (s) =>
+ s.stdout
+ .split("\n")
+ .map((substr) => substr.trim())
+ .filter((n) => n),
+ ) as string[]
if (output.length < 5) {
// do it again but with more violence
console.log(
`-kmeans 10 of ${image.name} didn't give enough colours, trying again with a higher value...`,
)
- output =
- await $`magick ${path} -colorspace Oklch -kmeans 40 -format "%c" histogram:info:`.then(
- (s) =>
- s.stdout
- .split("\n") // split the output by newlines
- .map((substr) => substr.trim()) // trim the whitespace on each line
- .filter((n) => n), // and then filter out any blank elements generated
- )
+ output = await $`magick ${path} -colorspace Oklch -kmeans 40 -format "%c" histogram:info:`.then(
+ (s) =>
+ s.stdout
+ .split("\n") // split the output by newlines
+ .map((substr) => substr.trim()) // trim the whitespace on each line
+ .filter((n) => n), // and then filter out any blank elements generated
+ )
}
const colours = output.map((entry) => {
@@ -87,7 +58,7 @@ export async function analyzeImages(imageDir: string, analysisOutput: ImageAnaly
return {
pixels: parseInt(arr[0]),
oklab: arr[1].replace(/[()]/g, "").split(","), // (0,0,0) -> Array [0, 0, 0]
- hex: arr[2]
+ hex: arr[2],
}
})
@@ -102,29 +73,7 @@ export async function analyzeImages(imageDir: string, analysisOutput: ImageAnaly
const chroma = Number(dominantColour[1]) * 100
const hue = Number(dominantColour[2])
- analysisOutput.lightnessData.forEach((n) => {
- const targetLightness = floorToStep(lightness, lightnessStep)
- if (n.lightness == targetLightness) {
- n.paths.push(path)
- return
- }
- })
-
- analysisOutput.chromaData.forEach((n) => {
- const targetChroma = floorToStep(chroma, chromaStep)
- if (n.chroma == targetChroma) {
- n.paths.push(path)
- return
- }
- })
-
- analysisOutput.hueData.forEach((n) => {
- const targetHue = floorToStep(hue, hueStep)
- if (n.hue == targetHue) {
- n.paths.push(path)
- return
- }
- })
+ analysisOutput.files.push({ path: path, oklch: [hue, chroma, lightness] })
// flush every time we finish analyzing a file, because it just takes so long
fs.writeFile(outputPath, JSON.stringify(analysisOutput, null, 4), (err: Error) => {
@@ -133,4 +82,3 @@ export async function analyzeImages(imageDir: string, analysisOutput: ImageAnaly
}
}
}
-
diff --git a/analyze/index.mts b/analyze/index.mts
index 90daead..5ad18d5 100755
--- a/analyze/index.mts
+++ b/analyze/index.mts
@@ -1,8 +1,8 @@
#!/usr/bin/env zx
///
-import { configDir, cacheDir, loadConfig } from "../utils.mts";
-import { analyzeImages, AnalysisConfig, ImageAnalysisData, runPreAnalysis, defaultConfig } from "./analysis.mts";
+import { cacheDir, configDir, loadConfig } from "../utils.mts"
+import { AnalysisConfig, analyzeImages, defaultConfig, ImageAnalysisData, runPreAnalysis } from "./analysis.mts"
const pathToConfig = `${configDir}/analyze-config.json`
@@ -11,15 +11,21 @@ console.debug(`Trying to load config from ${pathToConfig}...`)
const config = await loadConfig(pathToConfig, defaultConfig) as AnalysisConfig
if (config == undefined) {
- throw new Error("Couldn't initialize config!")
+ throw new Error("Couldn't initialize config!")
}
console.debug(`Config: \n${JSON.stringify(config, null, 4)}`)
-fs.rm(`${cacheDir}/analysis.json`, { force: true })
+await fs.cp(`${cacheDir}/analysis.json`, `${cacheDir}/analysis.json.prev`, { force: true }, (err: Error) => {
+ if (err) {
+ console.log(err)
+ }
+})
-const imageData = new ImageAnalysisData(config.lightnessStep, config.chromaStep, config.hueStep)
+const imageData = { files: [] } as ImageAnalysisData
await runPreAnalysis(config.preAnalysisCommands)
-await analyzeImages(config.imageDir, imageData, `${cacheDir}/analysis.json`, config.lightnessStep, config.chromaStep, config.hueStep)
\ No newline at end of file
+await analyzeImages(config.imageDir, imageData, `${cacheDir}/analysis.json`)
+
+await fs.rm(`${cacheDir}/analysis.json.prev`, { force: true })
diff --git a/apply/application.mts b/apply/application.mts
index c166a06..86e182b 100644
--- a/apply/application.mts
+++ b/apply/application.mts
@@ -1,7 +1,6 @@
#!/usr/bin/env zx
import { ImageAnalysisData } from "../analyze/analysis.mts"
-import _ from "npm:underscore"
///
@@ -85,23 +84,17 @@ export function findMatchingImages(
chromaValue: number,
lightnessValue: number,
) {
- let result = []
- let i = 0
- const chromaIndex = imagesData.chromaData.indexOf(
- imagesData.chromaData.find((data) => data.chroma === chromaValue)!,
- )
- const lightnessIndex = imagesData.lightnessData.indexOf(
- imagesData.lightnessData.find((data) => data.lightness === lightnessValue)!,
- )
-
- do {
- const matches = [
- imagesData.chromaData[chromaIndex + i].paths || imagesData.chromaData[chromaIndex - i].paths,
- imagesData.lightnessData[lightnessIndex + i].paths || imagesData.lightnessData[lightnessIndex - i].paths,
- ]
- console.log(matches)
- result = matches.reduce((a, b) => a.filter((c) => b.includes(c)))
+ const targetColour = [hueValue, chromaValue, lightnessValue]
+ let closestColour = { path: "", oklch: [0, 0, 0] }
- i++
- } while (result.length === 0)
+ for (const file of imagesData.files) {
+ if (
+ Math.abs(file.oklch[0] - targetColour[0]) <= Math.abs(closestColour.oklch[0] - targetColour[0]) &&
+ Math.abs(file.oklch[1] - targetColour[1]) <= Math.abs(closestColour.oklch[1] - targetColour[1]) &&
+ Math.abs(file.oklch[2] - targetColour[2]) <= Math.abs(closestColour.oklch[2] - targetColour[2])
+ ) {
+ closestColour = file
+ }
+ }
+ return closestColour
}
diff --git a/apply/index.mts b/apply/index.mts
index 2c8cd2b..6fc18f9 100644
--- a/apply/index.mts
+++ b/apply/index.mts
@@ -1,13 +1,12 @@
#!/usr/bin/env zx
///
-import { AnalysisConfig, defaultConfig as analysisDefaultConfig, ImageAnalysisData } from "../analyze/analysis.mts"
-import { cacheDir, configDir, floorToStep, loadConfig, map } from "../utils.mts"
+import { ImageAnalysisData } from "../analyze/analysis.mts"
+import { cacheDir, configDir, loadConfig, map, mapEaseInExpo, mapEaseOutExpo } from "../utils.mts"
import { ApplicationConfig, defaultConfig, findMatchingImages, getOpenMeteoData } from "./application.mts"
import * as SunCalc from "npm:suncalc"
const pathToConfig = `${configDir}/apply-config.json`
-const pathToAnalysisConfig = `${configDir}/analyze-config.json`
const pathToCache = `${cacheDir}/analysis.json`
console.debug(`Trying to load application config from ${pathToConfig}...`)
@@ -19,18 +18,6 @@ if (config == undefined) {
throw new Error("Couldn't initialize config!")
}
-// we also need the analysis config to load the steps
-console.debug(`Trying to load analysis config from ${pathToAnalysisConfig}...`)
-const analysisConfig = await loadConfig(
- pathToAnalysisConfig,
- analysisDefaultConfig,
-) as AnalysisConfig
-if (analysisConfig == undefined) {
- throw new Error("Couldn't initialize config!")
-}
-
-console.debug(`Config: \n${JSON.stringify(config, null, 4)}`)
-
const openMeteoData = await getOpenMeteoData(
config.latitude,
config.longitude,
@@ -48,31 +35,27 @@ console.info(
)
console.info(`Current sun altitude is ${sunCalcData.altitude}°!`)
-// const hueValue = clamp()
-const chromaValue = floorToStep(
- map(
- Number(openMeteoData.cloudCover),
- 0,
- 100,
- config.chromaRange.end,
- config.chromaRange.start,
- ),
- analysisConfig.chromaStep,
+const hueValue = Number(sunCalcData.altitude) >= 0
+ ? mapEaseOutExpo(Number(sunCalcData.altitude), 0, 90, 0, 264)
+ : mapEaseInExpo(Number(sunCalcData.altitude), 0, -90, 360, 264)
+const chromaValue = map(
+ Number(openMeteoData.cloudCover),
+ 0,
+ 100,
+ config.chromaRange.end,
+ config.chromaRange.start,
)
-
-const lightnessValue = floorToStep(
- map(
- Number(openMeteoData.shortwaveRadiation),
- 0,
- 1000,
- config.lightnessRange.start,
- config.lightnessRange.end,
- ),
- analysisConfig.lightnessStep,
+const lightnessValue = map(
+ Number(openMeteoData.shortwaveRadiation),
+ 0,
+ 1000,
+ config.lightnessRange.start,
+ config.lightnessRange.end,
)
-console.info(`Got chroma value ${chromaValue}!`)
-console.info(`Got lightness value ${lightnessValue}!`)
+console.info(`Got hue value ${hueValue}°!`)
+console.info(`Got chroma value ${chromaValue}%!`)
+console.info(`Got lightness value ${lightnessValue}%!`)
console.debug(`Trying to open cache file ${pathToCache}...`)
const cacheFile = await fs.promises.readFile(`${pathToCache}`, {
@@ -81,6 +64,8 @@ const cacheFile = await fs.promises.readFile(`${pathToCache}`, {
const imagesData = JSON.parse(cacheFile) as ImageAnalysisData
console.debug(`Opened cache file ${pathToCache}!`)
+const targetFile = findMatchingImages(imagesData, hueValue, chromaValue, lightnessValue)
-
-findMatchingImages(imagesData, 0, chromaValue, lightnessValue)
\ No newline at end of file
+console.info(`Found target image ${targetFile.path}!`)
+console.info(`Setting wallpaper...`)
+await $`eval ${config.applyWallpaperCommand.replace("%s", targetFile.path)}`
diff --git a/utils.mts b/utils.mts
index ac02769..ef122df 100644
--- a/utils.mts
+++ b/utils.mts
@@ -1,4 +1,3 @@
-
export const configDir = process.env.XDG_CONFIG_HOME
? `${process.env.XDG_CONFIG_HOME}/rainwall`
: `${os.homedir()}/.config/rainwall`
@@ -18,11 +17,11 @@ export async function loadConfig(pathToConfig: string, defaultConfig: object) {
let config: object | undefined
try {
const stringConfig = await fs.promises.readFile(`${pathToConfig}`, {
- encoding: "utf8"
+ encoding: "utf8",
})
if (stringConfig == undefined) {
console.warn(
- `Current config blank, generating a new one... you may need to stop this process and edit the config file.`
+ `Current config blank, generating a new one... you may need to stop this process and edit the config file.`,
)
config = defaultConfig
writeDefaultConfig(pathToConfig, defaultConfig)
@@ -33,7 +32,7 @@ export async function loadConfig(pathToConfig: string, defaultConfig: object) {
console.warn(
`Got ${
(err as Error).message
- } when trying to load config, generating a new one... you may need to stop this process and edit the config file.`
+ } when trying to load config, generating a new one... you may need to stop this process and edit the config file.`,
)
config = defaultConfig
writeDefaultConfig(pathToConfig, defaultConfig)
@@ -47,14 +46,26 @@ async function writeDefaultConfig(path: string, data: object) {
console.info(`Generated default config at ${path}!`)
}
-export function floorToStep(num: number, step: number) {
- return Math.floor(num / step) * step
-}
-
export function clamp(number: number, min: number, max: number) {
return Math.max(min, Math.min(number, max))
}
export function map(number: number, min: number, max: number, newMin: number, newMax: number) {
- return (((number - min) / (max - min)) * (newMax - newMin)) + newMin
-}
\ No newline at end of file
+ return ((number - min) / (max - min)) * (newMax - newMin) + newMin
+}
+
+export function mapEaseInExpo(number: number, min: number, max: number, newMin: number, newMax: number) {
+ return easeInExpo((number - min) / (max - min)) * (newMax - newMin) + newMin
+}
+
+export function mapEaseOutExpo(number: number, min: number, max: number, newMin: number, newMax: number) {
+ return easeOutExpo((number - min) / (max - min)) * (newMax - newMin) + newMin
+}
+
+function easeInExpo(x: number): number {
+ return x === 0 ? 0 : Math.pow(2, 10 * x - 10)
+}
+
+function easeOutExpo(x: number): number {
+ return x === 1 ? 1 : 1 - Math.pow(2, -10 * x)
+}