217 lines
6.1 KiB
HTML
217 lines
6.1 KiB
HTML
<!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>
|