🚧 wip: chekkupointo

This commit is contained in:
2026-02-19 23:47:28 +01:00
parent fff2e56467
commit 336105eee8
17 changed files with 611 additions and 120 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(Wallpaper_Carousel VERSION 0.1 LANGUAGES CXX) project(WallReel VERSION 0.1 LANGUAGES CXX)
set(EXECUTABLE_NAME "wallreel") set(EXECUTABLE_NAME "wallreel")
set(CORELIB_NAME "wallreel-core") set(CORELIB_NAME "wallreel-core")
+1 -1
View File
@@ -89,7 +89,7 @@ void TestConfigMgr::testDefaults() {
QVERIFY(config.getActionConfig().onSelected.isEmpty()); QVERIFY(config.getActionConfig().onSelected.isEmpty());
QVERIFY(config.getActionConfig().onPreview.isEmpty()); QVERIFY(config.getActionConfig().onPreview.isEmpty());
QVERIFY(config.getActionConfig().onRestore.isEmpty()); QVERIFY(config.getActionConfig().onRestore.isEmpty());
QVERIFY(config.getActionConfig().saveState.isEmpty()); QVERIFY(config.getActionConfig().saveStateConfig.isEmpty());
} }
void TestConfigMgr::testFullConfigParsing() { void TestConfigMgr::testFullConfigParsing() {
+1
View File
@@ -7,6 +7,7 @@ qt_add_qml_module(${CORELIB_NAME}
Image/provider.hpp Image/provider.cpp Image/provider.hpp Image/provider.cpp
Palette/data.hpp Palette/data.hpp
Palette/manager.hpp Palette/manager.cpp Palette/manager.hpp Palette/manager.cpp
Config/data.hpp
Config/manager.hpp Config/manager.cpp Config/manager.hpp Config/manager.cpp
logger.hpp logger.cpp logger.hpp logger.cpp
wallpaperservice.hpp wallpaperservice.cpp wallpaperservice.hpp wallpaperservice.cpp
+119
View File
@@ -0,0 +1,119 @@
#ifndef WALLREEL_CONFIG_DATA_HPP
#define WALLREEL_CONFIG_DATA_HPP
#include <QColor>
#include <QHash>
#include <QList>
#include <QRegularExpression>
#include <QSize>
#include <QString>
#include <QStringList>
// Config entries:
//
// wallpaper.paths array [] List of paths to images.
// wallpaper.dirs array [] Directories to search for images.
// wallpaper.dirs[].path string "" Path to the directory.
// wallpaper.dirs[].recursive boolean false Whether to search the directory recursively.
// wallpaper.excludes array [] Exclude patterns (regex)
//
// palettes array []
// palettes[].name string "" Name of the palette
// palettes[].colors array [] List of colors in the palette
// palettes[].colors[].name string "" Name of the color
// palettes[].colors[].value string "" Color value in hex format, e.g. "#ff0000" for red
//
// action.previewDebounceTime number 300 Debounce time for preview action in milliseconds
// action.printSelected boolean true Whether to print the selected wallpaper path to stdout on confirm
// action.printPreview boolean false Whether to print the previewed wallpaper path to stdout on preview
// action.onSelected string "" Command to execute on confirmation ({{ path }} -> full path)
// action.onPreview string "" Command to execute on preview ({{ path }} -> full path)
// action.saveState array [] Useful for restore command
// action.saveState[].key string "" Key of value to save, used as {{ key }} in onRestore command
// action.saveState[].default string "" Value to save, used when "cmd" is not set or command execution fails or output is empty
// action.saveState[].cmd string "" Command that outputs(to stdout) the value to save when executed
// action.saveState[].timeout number 3000 Timeout for executing "cmd" in milliseconds. 0 or negative means no timeout
// action.onRestore string "" Command to execute on restore ({{ key }} -> value defined or obtained in saveState)
//
// style.image_width number 320 Width of each image
// style.image_height number 200 Height of each image
// style.image_focus_scale number 1.5 Scale of the focused image (relative to unfocused image)
// style.window_width number 750 Initial window width
// style.window_height number 500 Initial window height
//
// sort.type string "name" Sorting type: "none", "name", "date", "size"
// sort.reverse boolean false Whether to reverse the sorting order
// Normal order: name: lexicographical, e.g. "a.jpg" before "b.jpg"
// date: older before newer
// size: smaller before larger
namespace WallReel::Core::Config {
inline const QString s_DefaultConfigFileName = "config.json";
enum class SortType : int {
None = 0, // "none"
Name, // "name"
Date, // "date"
Size, // "size"
};
struct WallpaperConfigItems {
struct WallpaperDirConfigItem {
QString path;
bool recursive;
};
QStringList paths;
QList<WallpaperDirConfigItem> dirs;
QList<QRegularExpression> excludes;
};
struct PaletteConfigItems {
struct PaletteColorConfigItem {
QString name;
QColor value;
};
struct PaletteConfigItem {
QString name;
QList<PaletteColorConfigItem> colors;
};
QList<PaletteConfigItem> palettes;
};
struct ActionConfigItems {
struct SaveStateItem {
QString key;
QString defaultVal;
QString cmd;
int timeout = 3000;
};
QList<SaveStateItem> saveStateConfig;
QHash<QString, QString> saveState;
QString onSelected;
QString onPreview;
QString onRestore;
int previewDebounceTime = 300; // milliseconds
bool printSelected = true;
bool printPreview = false;
};
struct StyleConfigItems {
double imageFocusScale = 1.5;
int imageWidth = 320;
int imageHeight = 200;
int windowWidth = 750;
int windowHeight = 500;
};
struct SortConfigItems {
SortType type = SortType::Name;
bool reverse = false;
};
} // namespace WallReel::Core::Config
#endif // WALLREEL_CONFIG_DATA_HPP
+118 -7
View File
@@ -5,8 +5,10 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QProcess>
#include <QProcessEnvironment> #include <QProcessEnvironment>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTimer>
#include "Utils/misc.hpp" #include "Utils/misc.hpp"
#include "logger.hpp" #include "logger.hpp"
@@ -179,13 +181,27 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
m_actionConfig.printPreview = val.toBool(); m_actionConfig.printPreview = val.toBool();
} }
} }
if (config.contains("saveState")) { if (config.contains("saveState") && config["saveState"].isArray()) {
const auto& val = config["saveState"]; const QJsonArray& arr = config["saveState"].toArray();
if (val.isObject()) { for (const auto& item : arr) {
QJsonObject obj = val.toObject(); if (item.isObject()) {
for (const auto& key : obj.keys()) { QJsonObject obj = item.toObject();
if (obj[key].isString()) { ActionConfigItems::SaveStateItem sItem;
m_actionConfig.saveState.insert(key, obj[key].toString()); if (obj.contains("key") && obj["key"].isString()) {
sItem.key = obj["key"].toString();
}
if (obj.contains("default") && obj["default"].isString()) {
sItem.defaultVal = obj["default"].toString();
}
if (obj.contains("cmd") && obj["cmd"].isString()) {
sItem.cmd = obj["cmd"].toString();
}
if (obj.contains("timeout") && obj["timeout"].isDouble()) {
sItem.timeout = obj["timeout"].toInt();
}
if (!sItem.key.isEmpty()) {
m_actionConfig.saveStateConfig.append(sItem);
m_actionConfig.saveState.insert(sItem.key, sItem.defaultVal);
} }
} }
} }
@@ -339,3 +355,98 @@ void WallReel::Core::Config::Manager::_loadWallpapers() {
Logger::info(QString("Found %1 files").arg(paths.size())); Logger::info(QString("Found %1 files").arg(paths.size()));
} }
void WallReel::Core::Config::Manager::captureState() {
m_pendingCaptures = 0;
const auto& items = m_actionConfig.saveStateConfig;
if (items.isEmpty()) {
emit stateCaptured();
return;
}
for (const auto& item : items) {
if (!item.cmd.isEmpty()) {
m_pendingCaptures++;
}
}
if (m_pendingCaptures == 0) {
emit stateCaptured();
return;
}
for (const auto& item : items) {
if (item.cmd.isEmpty()) continue;
QProcess* process = new QProcess(this);
QTimer* timer = nullptr;
if (item.timeout > 0) {
timer = new QTimer(this);
timer->setSingleShot(true);
timer->setInterval(item.timeout > 0 ? item.timeout : std::numeric_limits<int>::max());
}
QString key = item.key;
QString defaultVal = item.defaultVal;
auto onFinished = [this, process, timer, key, defaultVal](const QString& output, bool success) {
if (timer) {
timer->stop();
timer->deleteLater();
}
process->disconnect();
QString result = success ? output : defaultVal;
if (result.isEmpty()) result = defaultVal;
_onCaptureResult(key, result);
process->deleteLater();
};
if (timer) {
// Timeout handler
connect(timer, &QTimer::timeout, this, [process, onFinished, key]() {
Logger::warn(QString("Timeout capturing state for key '%1'").arg(key));
if (process->state() != QProcess::NotRunning) {
process->kill();
} else {
onFinished(QString(), false);
}
});
}
// Finished handler
connect(process, &QProcess::finished, this, [process, onFinished](int exitCode, QProcess::ExitStatus exitStatus) {
bool success = (exitStatus == QProcess::NormalExit && exitCode == 0);
QString output;
if (success) {
output = QString::fromUtf8(process->readAllStandardOutput()).trimmed();
}
onFinished(output, success);
});
// Error handler
connect(process, &QProcess::errorOccurred, this, [process, onFinished, key](QProcess::ProcessError error) {
if (error == QProcess::FailedToStart) {
Logger::warn(QString("Failed to start state command for key '%1'").arg(key));
onFinished(QString(), false);
}
});
if (timer) {
timer->start();
}
process->startCommand(item.cmd);
}
}
void WallReel::Core::Config::Manager::_onCaptureResult(const QString& key, const QString& value) {
// This is all in main thread, so no lock needed
m_actionConfig.saveState[key] = value;
m_pendingCaptures--;
if (m_pendingCaptures == 0) {
emit stateCaptured();
}
}
+9 -99
View File
@@ -1,107 +1,10 @@
#ifndef WALLREEL_CONFIGMGR_HPP #ifndef WALLREEL_CONFIGMGR_HPP
#define WALLREEL_CONFIGMGR_HPP #define WALLREEL_CONFIGMGR_HPP
#include <QColor> #include "data.hpp"
#include <QObject>
#include <QRegularExpression>
#include <QSize>
#include <QString>
#include <QStringList>
// Config entries:
//
// wallpaper.paths array [] List of paths to images.
// wallpaper.dirs array [] Directories to search for images.
// wallpaper.dirs[].path string "" Path to the directory.
// wallpaper.dirs[].recursive boolean false Whether to search the directory recursively.
// wallpaper.excludes array [] Exclude patterns (regex)
//
// palettes array []
// palettes[].name string "" Name of the palette
// palettes[].colors array [] List of colors in the palette
// palettes[].colors[].name string "" Name of the color
// palettes[].colors[].value string "" Color value in hex format, e.g. "#ff0000" for red
//
// action.previewDebounceTime number 300 Minimum debounce time for preview action in milliseconds
// action.printSelected boolean false Whether to print the selected wallpaper path to stdout on confirm
// action.printPreview boolean false Whether to print the previewed wallpaper path to stdout on preview
// action.saveState object {} Key-value pairs to save the state, useful for restore command
// action.onRestore string "" Command to execute on restore ({{ key }} -> value in saveState)
// action.onSelected string "" Command to execute on confirmation ({{ path }} -> full path)
// action.onPreview string "" Command to execute on preview ({{ path }} -> full path)
//
// style.image_width number 320 Width of each image
// style.image_height number 200 Height of each image
// style.image_focus_scale number 1.5 Scale of the focused image (relative to unfocused image)
// style.window_width number 750 Initial window width
// style.window_height number 500 Initial window height
//
// sort.type string "name" Sorting type: "none", "name", "date", "size"
// sort.reverse boolean false Whether to reverse the sorting order
// Normal order: name: lexicographical, e.g. "a.jpg" before "b.jpg"
// date: older before newer
// size: smaller before larger
namespace WallReel::Core::Config { namespace WallReel::Core::Config {
static const QString s_DefaultConfigFileName = "config.json";
enum class SortType : int {
None = 0, // "none"
Name, // "name"
Date, // "date"
Size, // "size"
};
struct WallpaperConfigItems {
struct WallpaperDirConfigItem {
QString path;
bool recursive;
};
QStringList paths;
QList<WallpaperDirConfigItem> dirs;
QList<QRegularExpression> excludes;
};
struct PaletteConfigItems {
struct PaletteColorConfigItem {
QString name;
QColor value;
};
struct PaletteConfigItem {
QString name;
QList<PaletteColorConfigItem> colors;
};
QList<PaletteConfigItem> palettes;
};
struct ActionConfigItems {
QHash<QString, QString> saveState;
QString onSelected;
QString onPreview;
QString onRestore;
int previewDebounceTime = 300; // milliseconds
bool printSelected = false;
bool printPreview = false;
};
struct StyleConfigItems {
double imageFocusScale = 1.5;
int imageWidth = 320;
int imageHeight = 200;
int windowWidth = 750;
int windowHeight = 500;
};
struct SortConfigItems {
SortType type = SortType::Name;
bool reverse = false;
};
class Manager : public QObject { class Manager : public QObject {
Q_OBJECT Q_OBJECT
@@ -148,7 +51,10 @@ class Manager : public QObject {
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale; return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
} }
const QString m_configDir; Q_INVOKABLE void captureState();
signals:
void stateCaptured();
private: private:
void _loadConfig(const QString& configPath); void _loadConfig(const QString& configPath);
@@ -158,8 +64,10 @@ class Manager : public QObject {
void _loadActionConfig(const QJsonObject& config); void _loadActionConfig(const QJsonObject& config);
void _loadStyleConfig(const QJsonObject& config); void _loadStyleConfig(const QJsonObject& config);
void _loadSortConfig(const QJsonObject& config); void _loadSortConfig(const QJsonObject& config);
void _onCaptureResult(const QString& key, const QString& value);
private: private:
const QString m_configDir;
WallpaperConfigItems m_wallpaperConfig; WallpaperConfigItems m_wallpaperConfig;
PaletteConfigItems m_paletteConfig; PaletteConfigItems m_paletteConfig;
ActionConfigItems m_actionConfig; ActionConfigItems m_actionConfig;
@@ -167,6 +75,8 @@ class Manager : public QObject {
SortConfigItems m_sortConfig; SortConfigItems m_sortConfig;
QStringList m_wallpapers; QStringList m_wallpapers;
int m_pendingCaptures = 0;
}; };
} // namespace WallReel::Core::Config } // namespace WallReel::Core::Config
+1 -1
View File
@@ -6,7 +6,7 @@
#include <QTimer> #include <QTimer>
#include <atomic> #include <atomic>
#include "Config/manager.hpp" #include "Config/data.hpp"
#include "provider.hpp" #include "provider.hpp"
namespace WallReel::Core::Image { namespace WallReel::Core::Image {
+94
View File
@@ -0,0 +1,94 @@
#include "domcolor.hpp"
static constexpr int scaleMaxWidth = 128;
static constexpr int scaleMaxHeight = 128;
// See /misc/ColorArgTest/index.html for visualizing the effect of different powers
static constexpr double weightSaturationPower = 2.0;
static constexpr double weightLightnessPower = 2.0;
static constexpr int numBins = 36; // 360 degrees / 10 degrees per bin
static double getWeight(const QColor& color) {
int h, s, l;
color.getHsl(&h, &s, &l);
if (h < 0) return 0.0; // Skip undefined hues
double sNorm = s / 255.0;
double dist = std::abs(l - 128.0) / 128.0;
double lNorm = 1.0 - dist;
// // satPower=2, lightPower=2
// return std::pow(sNorm, weightSaturationPower) * std::pow(lNorm, weightLightnessPower);
// Since these are just simple squares, no need to use std::pow() here
return sNorm * sNorm * lNorm * lNorm;
}
static QColor getWeightedDominantColor(const QImage& image) {
if (image.isNull()) return QColor();
// QImage scaledImg = image.scaled(128, 128, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
QImage* scaledImg = nullptr;
if (image.width() > ::scaleMaxWidth || image.height() > ::scaleMaxHeight) {
scaledImg = new QImage(image.scaled(::scaleMaxWidth, ::scaleMaxHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
} else {
scaledImg = new QImage(image);
}
struct HueBin {
double totalWeight = 0.0;
double sumR = 0.0;
double sumG = 0.0;
double sumB = 0.0;
};
QVector<HueBin> bins(::numBins);
for (int y = 0; y < scaledImg->height(); ++y) {
for (int x = 0; x < scaledImg->width(); ++x) {
QColor color = scaledImg->pixelColor(x, y);
int hue = color.hue();
if (hue < 0) continue; // Skip grayscale pixels
double weight = getWeight(color);
// Filter out low-weight pixels to reduce noise
if (weight < 0.05) continue;
int binIndex = (hue / 10) % ::numBins;
bins[binIndex].totalWeight += weight;
bins[binIndex].sumR += color.redF() * weight;
bins[binIndex].sumG += color.greenF() * weight;
bins[binIndex].sumB += color.blueF() * weight;
}
}
delete scaledImg;
// Find the bin with the highest total weight
int maxBinIndex = -1;
double maxWeight = 0.0;
for (int i = 0; i < ::numBins; ++i) {
if (bins[i].totalWeight > maxWeight) {
maxWeight = bins[i].totalWeight;
maxBinIndex = i;
}
}
// If all filtered (e.g. grayscale image), return a default color
if (maxBinIndex == -1 || maxWeight <= 0.0) {
return QColor(Qt::gray);
}
// Calculate the average color for the winning bin
const HueBin& winningBin = bins[maxBinIndex];
int finalR = std::round((winningBin.sumR / winningBin.totalWeight) * 255.0);
int finalG = std::round((winningBin.sumG / winningBin.totalWeight) * 255.0);
int finalB = std::round((winningBin.sumB / winningBin.totalWeight) * 255.0);
return QColor(finalR, finalG, finalB);
}
QColor WallReel::Core::Palette::getDominantColor(const QImage& image) {
return ::getWeightedDominantColor(image);
}
+12
View File
@@ -0,0 +1,12 @@
#ifndef WALLREEL_CORE_PALETTE_DOMCOLOR_HPP
#define WALLREEL_CORE_PALETTE_DOMCOLOR_HPP
#include <QImage>
namespace WallReel::Core::Palette {
QColor getDominantColor(const QImage& image);
} // namespace WallReel::Core::Palette
#endif // WALLREEL_CORE_PALETTE_DOMCOLOR_HPP
+1 -1
View File
@@ -1,7 +1,7 @@
#ifndef WALLREEL_PALETTE_MANAGER_HPP #ifndef WALLREEL_PALETTE_MANAGER_HPP
#define WALLREEL_PALETTE_MANAGER_HPP #define WALLREEL_PALETTE_MANAGER_HPP
#include "Config/manager.hpp" #include "Config/data.hpp"
#include "data.hpp" #include "data.hpp"
namespace WallReel::Core::Palette { namespace WallReel::Core::Palette {
-1
View File
@@ -1 +0,0 @@
+1 -1
View File
@@ -4,7 +4,7 @@
#include <QProcess> #include <QProcess>
#include <QTimer> #include <QTimer>
#include "Config/manager.hpp" #include "Config/data.hpp"
#include "Image/data.hpp" #include "Image/data.hpp"
namespace WallReel::Core { namespace WallReel::Core {
+1
View File
@@ -6,4 +6,5 @@ qt_add_qml_module(${UILIB_NAME}
Pages/LoadingScreen.qml Pages/LoadingScreen.qml
Pages/CarouselScreen.qml Pages/CarouselScreen.qml
Modules/Carousel.qml Modules/Carousel.qml
Modules/TitleBar.qml
) )
+4 -4
View File
@@ -6,10 +6,10 @@ import WallReel.UI.Pages
ApplicationWindow { ApplicationWindow {
width: Config.windowWidth width: Config.windowWidth
height: Config.windowHeight height: Config.windowHeight
minimumWidth: width // minimumWidth: width
maximumWidth: width // maximumWidth: width
minimumHeight: height // minimumHeight: height
maximumHeight: height // maximumHeight: height
visible: true visible: true
title: qsTr("Hello World") title: qsTr("Hello World")
+27
View File
@@ -0,0 +1,27 @@
import QtQuick
import QtQuick.Controls
Item {
id: root
property string title: ""
// start from 0
property int index: 0
property int totalCount: 0
property int maxTitleLength: 50
Label {
text: (root.index + 1) + " / " + root.totalCount
font.pixelSize: 12
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
}
Label {
text: root.title.length > root.maxTitleLength ? root.title.substring(0, root.maxTitleLength) + "..." : root.title
font.pixelSize: 12
anchors.centerIn: parent
}
}
+5 -4
View File
@@ -41,10 +41,11 @@ Item {
anchors.margins: 20 anchors.margins: 20
spacing: 20 spacing: 20
Label { TitleBar {
text: (ImageModel.dataAt(carousel.currentIndex, "imgName") ?? "") + " (" + (carousel.currentIndex + 1) + "/" + carousel.count + ")" title: ImageModel.dataAt(carousel.currentIndex, "imgName") ?? ""
font.pixelSize: 12 index: carousel.currentIndex
Layout.alignment: Qt.AlignHCenter totalCount: carousel.count
Layout.fillWidth: true
} }
Carousel { Carousel {
+216
View File
@@ -0,0 +1,216 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Color Arg Test</title>
<style>
body {
display: flex;
font-family: sans-serif;
}
.left-panel {
flex: 1;
padding: 20px;
border-right: 1px solid #ccc;
}
.right-panel {
flex: 1;
padding: 20px;
}
#image-preview {
max-width: 100%;
max-height: 500px;
margin-top: 10px;
display: none;
}
.control-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
#color-box {
width: 100px;
height: 100px;
border: 1px solid #000;
margin-top: 20px;
background-color: #ccc;
}
</style>
</head>
<body>
<div class="left-panel">
<input type="file" id="image-upload" accept="image/*" />
<br />
<img id="image-preview" alt="Image Preview" />
</div>
<div class="right-panel">
<div class="control-group">
<label for="saturation-power">Saturation Power: <span id="sat-val">1.0</span></label>
<input type="range" id="saturation-power" min="1" max="5" step="0.1" value="1" />
</div>
<div class="control-group">
<label for="lightness-power">Lightness Power: <span id="light-val">1.0</span></label>
<input type="range" id="lightness-power" min="1" max="5" step="0.1" value="1" />
</div>
<div id="color-box"></div>
</div>
<canvas id="hidden-canvas" style="display: none"></canvas>
<script>
const imageUpload = document.getElementById('image-upload');
const imagePreview = document.getElementById('image-preview');
const hiddenCanvas = document.getElementById('hidden-canvas');
const ctx = hiddenCanvas.getContext('2d');
const satSlider = document.getElementById('saturation-power');
const lightSlider = document.getElementById('lightness-power');
const satValDisplay = document.getElementById('sat-val');
const lightValDisplay = document.getElementById('light-val');
const colorBox = document.getElementById('color-box');
let currentPixelBuffer = null;
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h,
s,
l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h * 360, s * 255, l * 255];
}
function foobar(rgbaBuffer, satPower, lightPower) {
const numBins = 36;
const bins = Array.from({ length: numBins }, () => ({
totalWeight: 0,
sumR: 0,
sumG: 0,
sumB: 0,
}));
for (let i = 0; i < rgbaBuffer.length; i += 4) {
const r = rgbaBuffer[i];
const g = rgbaBuffer[i + 1];
const b = rgbaBuffer[i + 2];
const a = rgbaBuffer[i + 3];
if (a < 5) continue;
const [h, s, l] = rgbToHsl(r, g, b);
if (s === 0) continue;
const sNorm = s / 255.0;
const wS = Math.pow(sNorm, satPower);
const dist = Math.abs(l - 128.0) / 128.0;
const lNorm = 1.0 - dist;
const wL = Math.pow(lNorm, lightPower);
const weight = wS * wL;
if (weight < 0.05) continue;
const binIndex = Math.floor(h / 10) % numBins;
bins[binIndex].totalWeight += weight;
bins[binIndex].sumR += r * weight;
bins[binIndex].sumG += g * weight;
bins[binIndex].sumB += b * weight;
}
let maxBinIndex = -1;
let maxWeight = 0;
for (let i = 0; i < numBins; i++) {
if (bins[i].totalWeight > maxWeight) {
maxWeight = bins[i].totalWeight;
maxBinIndex = i;
}
}
if (maxBinIndex === -1 || maxWeight <= 0) {
return `rgb(128, 128, 128)`;
}
const winningBin = bins[maxBinIndex];
const finalR = Math.round(winningBin.sumR / winningBin.totalWeight);
const finalG = Math.round(winningBin.sumG / winningBin.totalWeight);
const finalB = Math.round(winningBin.sumB / winningBin.totalWeight);
return `rgb(${finalR}, ${finalG}, ${finalB})`;
}
function updateColor() {
if (!currentPixelBuffer) return;
const satPower = parseFloat(satSlider.value);
const lightPower = parseFloat(lightSlider.value);
satValDisplay.textContent = satPower.toFixed(1);
lightValDisplay.textContent = lightPower.toFixed(1);
const color = foobar(currentPixelBuffer, satPower, lightPower);
colorBox.style.backgroundColor = color;
}
imageUpload.addEventListener('change', function (e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function (event) {
const img = new Image();
img.onload = function () {
imagePreview.src = event.target.result;
imagePreview.style.display = 'block';
hiddenCanvas.width = img.width;
hiddenCanvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
currentPixelBuffer = imageData.data;
updateColor();
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
satSlider.addEventListener('input', updateColor);
lightSlider.addEventListener('input', updateColor);
</script>
</body>
</html>