File: C:/xampp/htdocs/koku/Aesthetic_cal.html
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>KOKU|配色美度係數計算工具(含色盤挑色)</title>
<style>
body { font-family: Arial, "Microsoft JhengHei"; margin: 40px; background: #f3f3f3; }
h1 { color: #333; }
textarea { width: 100%; font-size: 15px; padding: 10px; box-sizing: border-box; }
#hexInput { height: 90px; }
#munsellInput { height: 90px; margin-top: 10px; }
button { padding: 8px 16px; font-size: 14px; margin-top: 10px; cursor: pointer; }
.result { background: #fff; padding: 20px; margin-top: 20px; border-radius: 8px; }
table { width: 100%; border-collapse: collapse; margin-top: 15px; }
td, th { border: 1px solid #ccc; padding: 6px; font-size: 14px; text-align: center; }
.note { font-size: 13px; color: #555; }
.swatch { width: 50px; height: 22px; border: 1px solid #ccc; margin: 0 auto; }
.picker-row { display: flex; align-items: center; gap: 10px; margin-top: 6px; }
.picker-row input[type="text"] { width: 90px; }
</style>
</head>
<body>
<h1>🎨 KOKU|配色美度係數計算工具</h1>
<!-- 方式一:HEX 文字輸入 -->
<p><b>方式一:輸入 HEX 色碼(空白分隔)</b></p>
<textarea id="hexInput" placeholder="#FF0000 #00FF00 #0000FF"></textarea>
<p class="note">例:<code>#FF0000 #00FF00 #0000FF</code></p>
<!-- 方式二:Munsell 文字輸入 -->
<p><b>方式二:直接輸入孟塞爾色碼(每行一個)</b></p>
<textarea id="munsellInput" placeholder="10RP 4/12
5R 5/14
9BG 6.5/10
6YR 5.5/12"></textarea>
<p class="note">
格式:<code>色相 值/彩度</code>,例如 <code>5R 5/12</code>、<code>10RP 4/14</code>、<code>9BG 6.5/10</code>。<br>
色相的數字不一定要是 2.5/5/7.5/10,程式會自動「靠最近一格」對齊到官方 40 等分色相。
</p>
<!-- 方式三:色盤挑色 -->
<p><b>方式三:用色盤選色</b></p>
<div id="pickerList"></div>
<button type="button" onclick="addPicker()">➕ 新增一個顏色</button>
<p class="note">每個色盤會自動把 HEX 納入計算;你可以同時用 HEX/Munsell/色盤三種方式混搭。</p>
<hr>
<button onclick="run()">開始計算美度</button>
<div id="output" class="result"></div>
<script>
/* =========================================================
0. 初始化:先給兩個色盤
========================================================= */
window.onload = function() {
addPicker("#FF0000");
addPicker("#00FF00");
};
/* 動態新增色盤 */
function addPicker(defaultHex) {
const list = document.getElementById("pickerList");
const row = document.createElement("div");
row.className = "picker-row";
const colorInput = document.createElement("input");
colorInput.type = "color";
colorInput.value = defaultHex || "#FF0000";
const hexInput = document.createElement("input");
hexInput.type = "text";
hexInput.className = "picker-hex";
hexInput.value = colorInput.value.toUpperCase();
// color 改變時同步到文字
colorInput.addEventListener("input", () => {
hexInput.value = colorInput.value.toUpperCase();
});
// 文字改變時嘗試同步到 color(格式錯了就忽略)
hexInput.addEventListener("change", () => {
let v = hexInput.value.trim();
if (!v.startsWith("#")) v = "#" + v;
if (/^#[0-9A-Fa-f]{6}$/.test(v)) {
colorInput.value = v;
hexInput.value = v.toUpperCase();
} else {
alert("HEX 格式錯誤,請輸入像 #FF0000 這種格式");
}
});
row.appendChild(colorInput);
row.appendChild(hexInput);
list.appendChild(row);
}
/* =========================================================
1. 曼塞爾 40 等分 Hue 列表(順時針)
========================================================= */
const MUNSELL_HUE_STEPS = [
"10RP","2.5R","5R","7.5R","10R",
"2.5YR","5YR","7.5YR","10YR",
"2.5Y","5Y","7.5Y","10Y",
"2.5GY","5GY","7.5GY","10GY",
"2.5G","5G","7.5G","10G",
"2.5BG","5BG","7.5BG","10BG",
"2.5B","5B","7.5B","10B",
"2.5PB","5PB","7.5PB","10PB",
"2.5P","5P","7.5P","10P",
"2.5RP","5RP"
];
/* 將任意數字(例如 9、6)對齊到最近的 2.5 / 5 / 7.5 / 10 */
function snapHueNumber(num) {
const steps = [2.5, 5, 7.5, 10];
let best = steps[0];
let bestDiff = Math.abs(num - steps[0]);
for (let i = 1; i < steps.length; i++) {
let d = Math.abs(num - steps[i]);
if (d < bestDiff) {
bestDiff = d;
best = steps[i];
}
}
return best;
}
/* =========================================================
2. HEX → XYZ → xyY → Munsell 近似
========================================================= */
function hexToRGB(hex) {
hex = hex.trim().replace("#", "");
if (hex.length !== 6) throw new Error("非法 HEX 色碼:" + hex);
return {
r: parseInt(hex.substring(0, 2), 16) / 255,
g: parseInt(hex.substring(2, 4), 16) / 255,
b: parseInt(hex.substring(4, 6), 16) / 255
};
}
// sRGB → XYZ(D65)
function rgbToXYZ(rgb) {
function c(v) { return v <= 0.04045 ? v/12.92 : Math.pow((v+0.055)/1.055, 2.4); }
let r = c(rgb.r), g = c(rgb.g), b = c(rgb.b);
return {
x: r*0.4124 + g*0.3576 + b*0.1805,
y: r*0.2126 + g*0.7152 + b*0.0722,
z: r*0.0193 + g*0.1192 + b*0.9505
};
}
// XYZ → xyY
function xyzToXyY(XYZ) {
let sum = XYZ.x + XYZ.y + XYZ.z;
if (sum === 0) return { x: 0.33, y: 0.33, Y: XYZ.y };
return { x: XYZ.x/sum, y: XYZ.y/sum, Y: XYZ.y };
}
/* xyY → Munsell 近似 */
function xyYtoMunsell(xyY) {
let V = Math.round(10 * Math.pow(xyY.Y, 1/3));
if (V < 0) V = 0;
if (V > 10) V = 10;
const angle = Math.atan2(xyY.y - 0.33, xyY.x - 0.33) * 180 / Math.PI;
let H = (angle + 360) % 360;
const hueSector = ["R","YR","Y","GY","G","BG","B","PB","P","RP"];
let index = Math.floor((H / 360) * 10);
let hue = hueSector[index % 10];
let C = Math.round(20 * Math.sqrt(Math.pow(xyY.x-0.33,2) + Math.pow(xyY.y-0.33,2)));
if (C < 0) C = 0;
return {
hueAngle: H,
hueCode: hue,
value: V,
chroma: C,
label: `${hue}${V}/${C}`
};
}
/* =========================================================
3. 色塊用:HSL → HEX
========================================================= */
function hslToHex(h, s, l) {
h = ((h % 360) + 360) % 360;
s = Math.max(0, Math.min(1, s));
l = Math.max(0, Math.min(1, l));
let c = (1 - Math.abs(2*l - 1)) * s;
let x = c * (1 - Math.abs((h / 60) % 2 - 1));
let m = l - c/2;
let r1, g1, b1;
if (h < 60) { r1=c; g1=x; b1=0; }
else if (h < 120){ r1=x; g1=c; b1=0; }
else if (h < 180){ r1=0; g1=c; b1=x; }
else if (h < 240){ r1=0; g1=x; b1=c; }
else if (h < 300){ r1=x; g1=0; b1=c; }
else { r1=c; g1=0; b1=x; }
function toHex(v) {
let n = Math.round((v + m) * 255);
return n.toString(16).padStart(2, "0");
}
return "#" + toHex(r1) + toHex(g1) + toHex(b1);
}
/* =========================================================
4. 解析孟塞爾色碼(自動對齊 40 格)
========================================================= */
function parseMunsellString(str) {
let raw = str.trim().toUpperCase();
if (!raw) return null;
// 灰階 N5
if (raw.startsWith("N")) {
let m = raw.match(/^N\s*([0-9.]+)/);
if (!m) throw new Error("無法解析孟塞爾色碼:" + raw);
let V = parseFloat(m[1]);
let H = 0;
let C = 0;
let hex = hslToHex(H, 0, V/10);
return {
source: "Munsell",
input: raw,
label: raw,
hueAngle: H,
value: V,
chroma: C,
displayColor: hex
};
}
// 彩色:數字+字母 + 值/彩度,例如 10RP 4/12、9BG 6.5/10
let m = raw.match(/^([0-9.]+)\s*([A-Z]+)\s+([0-9.]+)\/([0-9.]+)$/);
if (!m) throw new Error("無法解析孟塞爾色碼:" + raw);
let numPart = parseFloat(m[1]); // 10, 9, 6 ...
let letter = m[2]; // RP, R, YR, BG...
let V = parseFloat(m[3]);
let C = parseFloat(m[4]);
// 對齊到最近的 2.5 / 5 / 7.5 / 10
let snapped = snapHueNumber(numPart);
let numStr = snapped.toString(); // 2.5, 5, 7.5, 10
let fullHue = numStr + letter; // 例如 10RP, 5R, 10BG, 5YR ...
let idx = MUNSELL_HUE_STEPS.indexOf(fullHue);
let H;
if (idx >= 0) {
// 在 40 格表中找到:每一格 9 度
H = idx * 9;
} else {
// 萬一沒有對到,就退回粗略十等分
const hueMap = {
"R":0, "YR":36, "Y":72, "GY":108,
"G":144, "BG":180, "B":216, "PB":252,
"P":288, "RP":324
};
H = hueMap[letter] !== undefined ? hueMap[letter] : 0;
}
// 用 HSL 做一個大致接近的色塊
let s = Math.min(1, C / 12);
let l = V / 10;
let hex = hslToHex(H, s, l);
return {
source: "Munsell",
input: raw,
label: fullHue + " " + V + "/" + C + "(對齊後)",
hueAngle: H,
value: V,
chroma: C,
displayColor: hex
};
}
/* =========================================================
5. 依課本表格定義三個維度的係數
========================================================= */
function coeffHue(dh) {
if (dh === 0) return { cat: "相同", k: 1.5 };
if (dh >= 1 && dh <= 24) return { cat: "第一曖昧", k: 0.0 };
if (dh >= 25 && dh <= 42) return { cat: "類似", k: 1.1 };
if (dh >= 43 && dh <= 99) return { cat: "第二曖昧", k: 0.65 };
return { cat: "對比", k: 1.7 }; // ≥ 100°
}
function coeffValue(dv) {
if (dv === 0) return { cat: "相同", k: -1.3 };
if (dv >= 0.1 && dv <= 0.49) return { cat: "第一曖昧", k: -1.0 };
if (dv >= 0.5 && dv <= 1.49) return { cat: "類似", k: 0.7 };
if (dv >= 1.5 && dv <= 2.49) return { cat: "第二曖昧", k: -0.2 };
return { cat: "對比", k: 3.7 }; // ≥ 2.5
}
function coeffChroma(dc) {
if (dc === 0) return { cat: "相同", k: 0.8 };
if (dc >= 0.1 && dc <= 2.9) return { cat: "第一曖昧", k: 0.0 };
if (dc >= 3.0 && dc <= 4.9) return { cat: "類似", k: 0.1 };
if (dc >= 5.0 && dc <= 6.9) return { cat: "第二曖昧", k: 0.0 };
return { cat: "對比", k: 0.4 }; // ≥ 7.0
}
/* 兩色之間的整體分類與係數 */
function classifyPair(a, b) {
let dh = Math.abs(a.hueAngle - b.hueAngle);
if (dh > 180) dh = 360 - dh;
let dv = Math.abs(a.value - b.value);
let dc = Math.abs(a.chroma - b.chroma);
let hRes = coeffHue(dh);
let vRes = coeffValue(dv);
let cRes = coeffChroma(dc);
return {
dh, dv, dc,
relation: hRes.cat, // 用色相分類當代表
kH: hRes.k,
kV: vRes.k,
kC: cRes.k
};
}
/* =========================================================
6. 主流程:讀取輸入、計算、顯示結果
========================================================= */
function run() {
let hexText = document.getElementById("hexInput").value.trim();
let munText = document.getElementById("munsellInput").value.trim();
if (!hexText && !munText && document.querySelectorAll(".picker-hex").length === 0) {
alert("請至少輸入 HEX / 孟塞爾色碼或用色盤選色。");
return;
}
let data = [];
// 6-1. 處理 HEX 文字輸入
if (hexText) {
let hexes = hexText.split(/\s+/);
for (let h of hexes) {
if (!h.trim()) continue;
try {
let rgb = hexToRGB(h);
let xyz = rgbToXYZ(rgb);
let xyY = xyzToXyY(xyz);
let m = xyYtoMunsell(xyY);
data.push({
source: "HEX",
input: h,
label: m.label,
hueAngle: m.hueAngle,
value: m.value,
chroma: m.chroma,
displayColor: h.startsWith("#") ? h : ("#" + h)
});
} catch(e) {
alert("解析 HEX 失敗:" + h + ",錯誤:" + e.message);
return;
}
}
}
// 6-2. 處理色盤 HEX
const pickerHexes = document.querySelectorAll(".picker-hex");
pickerHexes.forEach(input => {
let v = input.value.trim();
if (!v) return;
if (!v.startsWith("#")) v = "#" + v;
if (!/^#[0-9A-Fa-f]{6}$/.test(v)) return; // 格式不對就忽略
try {
let rgb = hexToRGB(v);
let xyz = rgbToXYZ(rgb);
let xyY = xyzToXyY(xyz);
let m = xyYtoMunsell(xyY);
data.push({
source: "色盤HEX",
input: v.toUpperCase(),
label: m.label,
hueAngle: m.hueAngle,
value: m.value,
chroma: m.chroma,
displayColor: v.toUpperCase()
});
} catch(e) {
// 理論上不會進來
}
});
// 6-3. 處理直接輸入的孟塞爾
if (munText) {
let lines = munText.split(/\n+/);
for (let line of lines) {
if (!line.trim()) continue;
try {
let m = parseMunsellString(line);
data.push(m);
} catch(e) {
alert(e.message);
return;
}
}
}
if (data.length < 2) {
alert("至少需要兩個顏色才能計算美度。");
return;
}
// -------- 顏色清單與色塊 --------
let html = `<h2>轉換結果與色塊</h2><table><tr>
<th>來源</th><th>輸入</th><th>色塊</th><th>Munsell 標記</th><th>Hue°</th><th>V</th><th>C</th>
</tr>`;
data.forEach(d => {
html += `<tr>
<td>${d.source}</td>
<td>${d.input}</td>
<td><div class="swatch" style="background:${d.displayColor};"></div></td>
<td>${d.label}</td>
<td>${d.hueAngle.toFixed(1)}</td>
<td>${d.value}</td>
<td>${d.chroma}</td>
</tr>`;
});
// -------- 配色美度計算 --------
html += `</table><h2>色對與美度計算</h2>`;
let O = 0;
let pairCount = { h:0, v:0, c:0 };
html += `<table><tr><th>色對</th><th>ΔH</th><th>ΔV</th><th>ΔC</th><th>分類(依色相)</th><th>kH</th><th>kV</th><th>kC</th></tr>`;
for (let i = 0; i < data.length; i++) {
for (let j = i + 1; j < data.length; j++) {
let p = classifyPair(data[i], data[j]);
O += (p.kH + p.kV + p.kC);
if (p.dh > 0) pairCount.h++;
if (p.dv > 0) pairCount.v++;
if (p.dc > 0) pairCount.c++;
html += `<tr>
<td>${data[i].input} – ${data[j].input}</td>
<td>${p.dh.toFixed(1)}</td>
<td>${p.dv.toFixed(1)}</td>
<td>${p.dc.toFixed(1)}</td>
<td>${p.relation}</td>
<td>${p.kH}</td>
<td>${p.kV}</td>
<td>${p.kC}</td>
</tr>`;
}
}
let C = data.length + pairCount.h + pairCount.v + pairCount.c;
let M = O / C;
let statusColor = (M >= 0.5) ? "green" : "red";
let statusText = (M >= 0.5) ? "美度足(配色較和諧)" : "美度不足(配色較緊張/雜亂)";
html += `</table>
<h2>最終美度</h2>
<p>O(秩序因素)= <b>${O.toFixed(3)}</b></p>
<p>C(複雜因素)= <b>${C}</b></p>
<p>M(美度)=
<b style="font-size:24px;color:${statusColor};">${M.toFixed(3)}</b>
<span style="color:${statusColor};font-weight:bold;margin-left:8px;">${statusText}</span>
</p>
<p class="note">註:HEX ↔ 孟塞爾與色塊為近似換算,用於教學與配色美度評估,非精密量測用途。</p>
`;
document.getElementById("output").innerHTML = html;
}
</script>
</body>
</html>