File size: 3,575 Bytes
d7da259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { rgbToLab, labDist } from './colorUtils.js';

/**
 * CIEDE2000 color difference.
 * More perceptually uniform than Euclidean Lab distance.
 */
export function ciede2000(lab1, lab2) {
  const [L1, a1, b1] = lab1;
  const [L2, a2, b2] = lab2;

  const C1 = Math.sqrt(a1 * a1 + b1 * b1);
  const C2 = Math.sqrt(a2 * a2 + b2 * b2);
  const Cmean = (C1 + C2) / 2;
  const Cmean7 = Cmean ** 7;
  const G = 0.5 * (1 - Math.sqrt(Cmean7 / (Cmean7 + 6103515625))); // 25^7

  const a1p = a1 * (1 + G);
  const a2p = a2 * (1 + G);
  const C1p = Math.sqrt(a1p * a1p + b1 * b1);
  const C2p = Math.sqrt(a2p * a2p + b2 * b2);

  let h1p = Math.atan2(b1, a1p) * 180 / Math.PI;
  if (h1p < 0) h1p += 360;
  let h2p = Math.atan2(b2, a2p) * 180 / Math.PI;
  if (h2p < 0) h2p += 360;

  const dLp = L2 - L1;
  const dCp = C2p - C1p;

  let dhp;
  if (C1p * C2p === 0) {
    dhp = 0;
  } else if (Math.abs(h2p - h1p) <= 180) {
    dhp = h2p - h1p;
  } else if (h2p - h1p > 180) {
    dhp = h2p - h1p - 360;
  } else {
    dhp = h2p - h1p + 360;
  }
  const dHp = 2 * Math.sqrt(C1p * C2p) * Math.sin(dhp * Math.PI / 360);

  const Lpmean = (L1 + L2) / 2;
  const Cpmean = (C1p + C2p) / 2;

  let Hpmean;
  if (C1p * C2p === 0) {
    Hpmean = h1p + h2p;
  } else if (Math.abs(h1p - h2p) <= 180) {
    Hpmean = (h1p + h2p) / 2;
  } else if (h1p + h2p < 360) {
    Hpmean = (h1p + h2p + 360) / 2;
  } else {
    Hpmean = (h1p + h2p - 360) / 2;
  }

  const T = 1
    - 0.17 * Math.cos((Hpmean - 30) * Math.PI / 180)
    + 0.24 * Math.cos(2 * Hpmean * Math.PI / 180)
    + 0.32 * Math.cos((3 * Hpmean + 6) * Math.PI / 180)
    - 0.20 * Math.cos((4 * Hpmean - 63) * Math.PI / 180);

  const SL = 1 + 0.015 * (Lpmean - 50) ** 2 / Math.sqrt(20 + (Lpmean - 50) ** 2);
  const SC = 1 + 0.045 * Cpmean;
  const SH = 1 + 0.015 * Cpmean * T;

  const Cpmean7 = Cpmean ** 7;
  const RT = -2 * Math.sqrt(Cpmean7 / (Cpmean7 + 6103515625))
    * Math.sin(60 * Math.exp(-(((Hpmean - 275) / 25) ** 2)) * Math.PI / 180);

  return Math.sqrt(
    (dLp / SL) ** 2 +
    (dCp / SC) ** 2 +
    (dHp / SH) ** 2 +
    RT * (dCp / SC) * (dHp / SH)
  );
}

/**
 * Assign stroke colors to anchor groups using nearest-anchor distance.
 *
 * @param {{ anchors: {hex,r,g,b}[], strokes: {hex,r,g,b}[] }} structure
 * @param {{ hex: string, r: number, g: number, b: number }[]} allColors
 * @param {{ distFn?: 'lab' | 'ciede2000' }} options
 * @returns {number[][]} groups - array of color index arrays (anchor order)
 */
export function buildStructuralColorGroups(structure, allColors, options = {}) {
  const { anchors, strokes } = structure;
  const distFn = options.distFn || 'ciede2000';

  // Build color index map
  const colorIndex = new Map();
  allColors.forEach((c, i) => colorIndex.set(c.hex, i));

  // Compute anchor Labs
  const anchorLabs = anchors.map(a => rgbToLab(a.r, a.g, a.b));

  // Initialize groups with anchor colors
  const groups = anchors.map(a => {
    const idx = colorIndex.get(a.hex);
    return idx !== undefined ? [idx] : [];
  });

  // Distance function
  const dist = distFn === 'ciede2000' ? ciede2000 : labDist;

  // Assign each stroke to nearest anchor
  for (const s of strokes) {
    const idx = colorIndex.get(s.hex);
    if (idx === undefined) continue;
    const sLab = rgbToLab(s.r, s.g, s.b);
    let bestGi = 0, bestDist = Infinity;
    for (let gi = 0; gi < anchorLabs.length; gi++) {
      const d = dist(sLab, anchorLabs[gi]);
      if (d < bestDist) { bestDist = d; bestGi = gi; }
    }
    groups[bestGi].push(idx);
  }

  return groups.filter(g => g.length > 0);
}