• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

willmendesneto / hex-to-css-filter / #128

11 Mar 2025 02:19PM UTC coverage: 90.0% (-5.4%) from 95.357%
#128

push

willmendesneto
6.0.0

62 of 81 branches covered (76.54%)

Branch coverage included in aggregate %.

190 of 199 relevant lines covered (95.48%)

27930.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.54
/src/solver.ts
1
import { Color } from './color';
2
import { HexToCssConfiguration } from './hex-to-css-filter';
1✔
3

1✔
4
interface SPSAPayload {
1✔
5
  /** How many times the script was called to solve the color */
1✔
6
  called?: number;
7
  /** Percentage loss value for the generated filter */
4✔
8
  loss: number;
4✔
9
  /** Percentage loss per each color type organized in RGB: red, green, blue, h, s, l. */
4✔
10
  values: [number, number, number, number, number, number];
11
}
12

13
class Solver {
14
  private target: Color;
15
  private targetHSL: { h: number; s: number; l: number };
16
  private reusedColor: Color;
17
  private options: { acceptanceLossPercentage: number; maxChecks: number } & HexToCssConfiguration;
18

19
  constructor(target: Color, options: HexToCssConfiguration) {
20
    this.target = target;
21
    this.targetHSL = target.hsl();
4✔
22

23
    this.options = Object.assign(
24
      {},
25
      // Adding default values for options
26
      {
27
        acceptanceLossPercentage: 5,
28
        maxChecks: 15,
29
      },
1✔
30
      options,
4✔
31
    );
4✔
32

33
    // All the calcs done by the library to generate
34
    // a CSS Filter are based on the color `#000`
35
    // in this case, `rgb(0, 0, 0)`
36
    // Please make sure the background of the element
37
    // is `#000` for better performance
38
    // and color similarity.
39
    this.reusedColor = new Color(0, 0, 0);
40
  }
41

42
  /**
43
   * Returns the solved values for the
44
   *
45
   * @returns {(SPSAPayload & { filter: string; })}
1✔
46
   * @memberof Solver
4✔
47
   */
4✔
48
  solve(): SPSAPayload & {
49
    /** CSS filter generated based on the Hex color */
50
    filter: string;
4✔
51
  } {
4✔
52
    const result = this.solveNarrow(this.solveWide());
4✔
53
    return {
4✔
54
      values: result.values,
13✔
55
      called: result.called,
13✔
56
      loss: result.loss,
57
      filter: this.css(result.values),
58
    };
59
  }
60

61
  /**
62
   * Solve wide values based on the wide values for RGB and HSL values
63
   *
64
   * @private
13✔
65
   * @returns {SPSAPayload}
9✔
66
   * @memberof Solver
67
   */
13✔
68
  private solveWide(): SPSAPayload {
13!
69
    const A = 5;
×
70
    const c = 15;
71
    // Wide values for RGB and HSL values
72
    // the values in the order: [`r`, `g`, `b`, `h`, `s`, `l`]
4✔
73
    const a = [60, 180, 18000, 600, 1.2, 1.2];
74

75
    let best = { loss: Infinity };
76
    let counter = 0;
77
    while (best.loss > this.options.acceptanceLossPercentage) {
78
      const initialFilterValues: SPSAPayload['values'] = [50, 20, 3750, 50, 100, 100];
79
      const result: SPSAPayload = this.spsa({
80
        A,
81
        a,
82
        c,
1✔
83
        values: initialFilterValues,
4✔
84
        // for wide values we should use the double of tries in
4✔
85
        // comparison of `solveNarrow()` method
4✔
86
        maxTriesInLoop: 1000,
87
      });
88

4✔
89
      if (result.loss < best.loss) {
4✔
90
        best = result;
91
      }
92

93
      counter += 1;
94
      if (counter >= this.options.maxChecks) {
95
        break;
96
      }
97
    }
98

99
    return Object.assign({}, best, { called: counter }) as SPSAPayload;
100
  }
101

102
  /**
103
   * Solve narrow values based on the wide values for the filter
104
   *
105
   * @private
106
   * @param {SPSAPayload} wide
107
   * @returns {SPSAPayload}
108
   * @memberof Solver
109
   */
1✔
110
  private solveNarrow(wide: SPSAPayload): SPSAPayload {
90,000✔
111
    const A = wide.loss;
112
    const c = 2;
90,000✔
113
    const A1 = A + 1;
15,000✔
114
    // Narrow values for RGB and HSL values
115
    // the values in the order: [`r`, `g`, `b`, `h`, `s`, `l`]
75,000✔
116
    const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
30,000✔
117
    return this.spsa({
118
      A,
90,000✔
119
      a,
15,000✔
120
      c,
1,349✔
121
      values: wide.values,
122
      maxTriesInLoop: 500,
13,651✔
123
      called: wide.called,
1,344✔
124
    });
125
  }
126

127
  /**
128
   * Returns final value based on the current filter order
75,000✔
129
   * to get the order, please check the returned value
1,125✔
130
   * in `css()` method
131
   *
73,875✔
132
   * @private
920✔
133
   * @param {number} value
134
   * @param {number} idx
90,000✔
135
   * @returns {number}
136
   * @memberof Solver
1✔
137
   */
17!
138
  private fixValueByFilterIDX(value: number, idx: number): number {
17✔
139
    let max = 100;
17✔
140

17✔
141
    // Fixing max, minimum and value by filter
17✔
142
    if (idx === 2 /* saturate */) {
17✔
143
      max = 7500;
17✔
144
    } else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) {
17✔
145
      max = 200;
146
    }
17✔
147

17✔
148
    if (idx === 3 /* hue-rotate */) {
15,000✔
149
      if (value > max) {
15,000✔
150
        value %= max;
90,000✔
151
      } else if (value < 0) {
90,000✔
152
        value = max + (value % max);
90,000✔
153
      }
154
    }
15,000✔
155
    // Checking if value is below the minimum or above
15,000✔
156
    // the maximum allowed by filter
90,000✔
157
    else if (value < 0) {
90,000✔
158
      value = 0;
90,000✔
159
    } else if (value > max) {
160
      value = max;
15,000✔
161
    }
15,000✔
162
    return value;
568✔
163
  }
568✔
164

165
  private spsa({
166
    A,
17✔
167
    a,
168
    c,
169
    values,
170
    maxTriesInLoop = 500,
171
    called = 0,
172
  }: {
173
    A: number;
174
    a: number[];
175
    c: number;
176
    values: SPSAPayload['values'];
1✔
177
    maxTriesInLoop: number;
178
    called?: number;
45,000✔
179
  }): SPSAPayload {
180
    const alpha = 1;
181
    const gamma = 0.16666666666666666;
45,000✔
182

45,000✔
183
    let best = null;
45,000✔
184
    let bestLoss = Infinity;
45,000✔
185

45,000✔
186
    const deltas = new Array(6) as SPSAPayload['values'];
45,000✔
187
    const highArgs = new Array(6) as SPSAPayload['values'];
45,000✔
188
    const lowArgs = new Array(6) as SPSAPayload['values'];
45,000✔
189

45,000✔
190
    // Size of all CSS filters to be applied to get the correct color
191
    const filtersToBeAppliedSize = 6;
192

193
    for (let key = 0; key < maxTriesInLoop; key++) {
194
      const ck = c / Math.pow(key + 1, gamma);
195
      for (let i = 0; i < filtersToBeAppliedSize; i++) {
196
        deltas[i] = Math.random() > 0.5 ? 1 : -1;
197
        highArgs[i] = values[i] + ck * deltas[i];
198
        lowArgs[i] = values[i] - ck * deltas[i];
199
      }
200

201
      const lossDiff = this.loss(highArgs) - this.loss(lowArgs);
202
      for (let i = 0; i < filtersToBeAppliedSize; i++) {
203
        const g = (lossDiff / (2 * ck)) * deltas[i];
204
        const ak = a[i] / Math.pow(A + key + 1, alpha);
1✔
205
        values[i] = this.fixValueByFilterIDX(values[i] - ak * g, i);
4✔
206
      }
24✔
207

24✔
208
      const loss = this.loss(values);
209
      if (loss < bestLoss) {
4✔
210
        best = values.slice(0);
211
        bestLoss = loss;
212
      }
213
    }
214

215
    return { values: best, loss: bestLoss, called } as SPSAPayload;
216
  }
217

218
  /**
1✔
219
   * Checks how much is the loss for the filter in RGB and HSL colors
220
   *
1✔
221
   * @private
222
   * @param {SPSAPayload['values']} filters
223
   * @returns {number}
224
   * @memberof Solver
225
   */
226
  private loss(filters: SPSAPayload['values']): number {
227
    // Argument as an Array of percentages.
228
    const color = this.reusedColor;
229

230
    // Resetting the color to black in case
231
    // it was called more than once
232
    color.set(0, 0, 0);
233

234
    color.invert(filters[0] / 100);
235
    color.sepia(filters[1] / 100);
236
    color.saturate(filters[2] / 100);
237
    color.hueRotate(filters[3] * 3.6);
238
    color.brightness(filters[4] / 100);
239
    color.contrast(filters[5] / 100);
240

241
    const colorHSL = color.hsl();
242

243
    return (
244
      Math.abs(color.r - this.target.r) +
245
      Math.abs(color.g - this.target.g) +
246
      Math.abs(color.b - this.target.b) +
247
      Math.abs(colorHSL.h - this.targetHSL.h) +
248
      Math.abs(colorHSL.s - this.targetHSL.s) +
249
      Math.abs(colorHSL.l - this.targetHSL.l)
250
    );
251
  }
252

253
  /**
254
   * Returns the CSS filter list for the received HEX color
255
   *
256
   * @private
257
   * @param {number[]} filters
258
   * @returns {string}
259
   * @memberof Solver
260
   */
261
  private css(filters: number[]): string {
262
    const formatCssFilterValueByMultiplier = (idx: number, multiplier = 1): number =>
263
      Math.round(filters[idx] * multiplier);
264

265
    return [
266
      `invert(${formatCssFilterValueByMultiplier(0)}%)`,
267
      `sepia(${formatCssFilterValueByMultiplier(1)}%)`,
268
      `saturate(${formatCssFilterValueByMultiplier(2)}%)`,
269
      `hue-rotate(${formatCssFilterValueByMultiplier(3, 3.6)}deg)`,
270
      `brightness(${formatCssFilterValueByMultiplier(4)}%)`,
271
      `contrast(${formatCssFilterValueByMultiplier(5)}%)`,
272
    ].join(' ');
273
  }
274
}
275

276
export { Solver };
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc