HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
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>