@reely/colors 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * A class for applying and manipulating color transformations on an image using SVG filter-like operations.
3
+ *
4
+ * Inspired by:
5
+ * - https://www.getfishtank.com/insights/change-svg-colors-using-css-filters
6
+ * - https://codepen.io/sosuke/pen/Pjoqqp.
7
+ * - https://stackoverflow.com/questions/42966641/how-to-transform-black-into-any-given-color-using-only-css-filters/43960991#43960991
8
+ */
9
+ export declare class ImgSvgColorizeFilter {
10
+ r: number;
11
+ g: number;
12
+ b: number;
13
+ constructor(r: number, g: number, b: number);
14
+ toString(): string;
15
+ set(r: number, g: number, b: number): void;
16
+ hueRotate(angle?: number): void;
17
+ grayscale(value?: number): void;
18
+ sepia(value?: number): void;
19
+ saturate(value?: number): void;
20
+ multiply(matrix: number[]): void;
21
+ brightness(value?: number): void;
22
+ contrast(value?: number): void;
23
+ linear(slope?: number, intercept?: number): void;
24
+ invert(value?: number): void;
25
+ hsl(): {
26
+ h: number;
27
+ s: number;
28
+ l: number;
29
+ };
30
+ clamp(value: number): number;
31
+ }
32
+ export declare class Solver {
33
+ private target;
34
+ private targetHSL;
35
+ private readonly reusedColor;
36
+ constructor(target: ImgSvgColorizeFilter);
37
+ solve(): {
38
+ values: number[] | null;
39
+ loss: number;
40
+ filter: string;
41
+ };
42
+ solveWide(): {
43
+ loss: number;
44
+ values: number[] | null;
45
+ };
46
+ solveNarrow(wide: {
47
+ loss: number;
48
+ values: number[] | null;
49
+ }): {
50
+ values: number[] | null;
51
+ loss: number;
52
+ };
53
+ spsa(A: number, a: number[], c: number, values: number[] | null, iters: number): {
54
+ values: number[] | null;
55
+ loss: number;
56
+ };
57
+ loss(filters: number[]): number;
58
+ css(filters: number[]): string;
59
+ }
60
+ export declare function hexToRgb(hex: string): number[] | null;
61
+ //# sourceMappingURL=ImgSvgColorizeFilter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImgSvgColorizeFilter.d.ts","sourceRoot":"","sources":["../../src/lib/ImgSvgColorizeFilter.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,qBAAa,oBAAoB;IACxB,CAAC,SAAK;IACN,CAAC,SAAK;IACN,CAAC,SAAK;gBAED,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;IAIpC,QAAQ,IAAI,MAAM;IAIlB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI;IAM1C,SAAS,CAAC,KAAK,SAAI,GAAG,IAAI;IAkB1B,SAAS,CAAC,KAAK,SAAI,GAAG,IAAI;IAc1B,KAAK,CAAC,KAAK,SAAI,GAAG,IAAI;IActB,QAAQ,CAAC,KAAK,SAAI,GAAG,IAAI;IAczB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI;IAShC,UAAU,CAAC,KAAK,SAAI,GAAG,IAAI;IAI3B,QAAQ,CAAC,KAAK,SAAI,GAAG,IAAI;IAIzB,MAAM,CAAC,KAAK,SAAI,EAAE,SAAS,SAAI,GAAG,IAAI;IAMtC,MAAM,CAAC,KAAK,SAAI,GAAG,IAAI;IAMvB,GAAG,IAAI;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE;IAwC1C,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;CAQpC;AAED,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuB;gBAEvC,MAAM,EAAE,oBAAoB;IAMjC,KAAK,IAAI;QAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IASlE,SAAS,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;KAAE;IAgBtD,WAAW,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;KAAE,GAAG;QAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;IAQvG,IAAI,CACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EAAE,EACX,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EACvB,KAAK,EAAE,MAAM,GACZ;QAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE;IAwDrC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM;IAuB/B,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM;CAStC;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CASrD"}
@@ -0,0 +1,275 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ // @ts-nocheck
3
+ 'use strict';
4
+ /**
5
+ * A class for applying and manipulating color transformations on an image using SVG filter-like operations.
6
+ *
7
+ * Inspired by:
8
+ * - https://www.getfishtank.com/insights/change-svg-colors-using-css-filters
9
+ * - https://codepen.io/sosuke/pen/Pjoqqp.
10
+ * - https://stackoverflow.com/questions/42966641/how-to-transform-black-into-any-given-color-using-only-css-filters/43960991#43960991
11
+ */
12
+ export class ImgSvgColorizeFilter {
13
+ r = 0;
14
+ g = 0;
15
+ b = 0;
16
+ constructor(r, g, b) {
17
+ this.set(r, g, b);
18
+ }
19
+ toString() {
20
+ return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`;
21
+ }
22
+ set(r, g, b) {
23
+ this.r = this.clamp(r);
24
+ this.g = this.clamp(g);
25
+ this.b = this.clamp(b);
26
+ }
27
+ hueRotate(angle = 0) {
28
+ angle = (angle / 180) * Math.PI;
29
+ const sin = Math.sin(angle);
30
+ const cos = Math.cos(angle);
31
+ this.multiply([
32
+ 0.213 + cos * 0.787 - sin * 0.213,
33
+ 0.715 - cos * 0.715 - sin * 0.715,
34
+ 0.072 - cos * 0.072 + sin * 0.928,
35
+ 0.213 - cos * 0.213 + sin * 0.143,
36
+ 0.715 + cos * 0.285 + sin * 0.14,
37
+ 0.072 - cos * 0.072 - sin * 0.283,
38
+ 0.213 - cos * 0.213 - sin * 0.787,
39
+ 0.715 - cos * 0.715 + sin * 0.715,
40
+ 0.072 + cos * 0.928 + sin * 0.072,
41
+ ]);
42
+ }
43
+ grayscale(value = 1) {
44
+ this.multiply([
45
+ 0.2126 + 0.7874 * (1 - value),
46
+ 0.7152 - 0.7152 * (1 - value),
47
+ 0.0722 - 0.0722 * (1 - value),
48
+ 0.2126 - 0.2126 * (1 - value),
49
+ 0.7152 + 0.2848 * (1 - value),
50
+ 0.0722 - 0.0722 * (1 - value),
51
+ 0.2126 - 0.2126 * (1 - value),
52
+ 0.7152 - 0.7152 * (1 - value),
53
+ 0.0722 + 0.9278 * (1 - value),
54
+ ]);
55
+ }
56
+ sepia(value = 1) {
57
+ this.multiply([
58
+ 0.393 + 0.607 * (1 - value),
59
+ 0.769 - 0.769 * (1 - value),
60
+ 0.189 - 0.189 * (1 - value),
61
+ 0.349 - 0.349 * (1 - value),
62
+ 0.686 + 0.314 * (1 - value),
63
+ 0.168 - 0.168 * (1 - value),
64
+ 0.272 - 0.272 * (1 - value),
65
+ 0.534 - 0.534 * (1 - value),
66
+ 0.131 + 0.869 * (1 - value),
67
+ ]);
68
+ }
69
+ saturate(value = 1) {
70
+ this.multiply([
71
+ 0.213 + 0.787 * value,
72
+ 0.715 - 0.715 * value,
73
+ 0.072 - 0.072 * value,
74
+ 0.213 - 0.213 * value,
75
+ 0.715 + 0.285 * value,
76
+ 0.072 - 0.072 * value,
77
+ 0.213 - 0.213 * value,
78
+ 0.715 - 0.715 * value,
79
+ 0.072 + 0.928 * value,
80
+ ]);
81
+ }
82
+ multiply(matrix) {
83
+ const newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
84
+ const newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
85
+ const newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
86
+ this.r = newR;
87
+ this.g = newG;
88
+ this.b = newB;
89
+ }
90
+ brightness(value = 1) {
91
+ this.linear(value);
92
+ }
93
+ contrast(value = 1) {
94
+ this.linear(value, -(0.5 * value) + 0.5);
95
+ }
96
+ linear(slope = 1, intercept = 0) {
97
+ this.r = this.clamp(this.r * slope + intercept * 255);
98
+ this.g = this.clamp(this.g * slope + intercept * 255);
99
+ this.b = this.clamp(this.b * slope + intercept * 255);
100
+ }
101
+ invert(value = 1) {
102
+ this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
103
+ this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
104
+ this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
105
+ }
106
+ hsl() {
107
+ // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
108
+ const r = this.r / 255;
109
+ const g = this.g / 255;
110
+ const b = this.b / 255;
111
+ const max = Math.max(r, g, b);
112
+ const min = Math.min(r, g, b);
113
+ let h, s,
114
+ // eslint-disable-next-line prefer-const
115
+ l = (max + min) / 2;
116
+ if (max === min) {
117
+ h = s = 0;
118
+ }
119
+ else {
120
+ const d = max - min;
121
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
122
+ switch (max) {
123
+ case r:
124
+ h = (g - b) / d + (g < b ? 6 : 0);
125
+ break;
126
+ case g:
127
+ h = (b - r) / d + 2;
128
+ break;
129
+ case b:
130
+ h = (r - g) / d + 4;
131
+ break;
132
+ }
133
+ h /= 6;
134
+ }
135
+ return {
136
+ h: h * 100,
137
+ s: s * 100,
138
+ l: l * 100,
139
+ };
140
+ }
141
+ clamp(value) {
142
+ if (value > 255) {
143
+ value = 255;
144
+ }
145
+ else if (value < 0) {
146
+ value = 0;
147
+ }
148
+ return value;
149
+ }
150
+ }
151
+ export class Solver {
152
+ target;
153
+ targetHSL;
154
+ reusedColor;
155
+ constructor(target) {
156
+ this.target = target;
157
+ this.targetHSL = target.hsl();
158
+ this.reusedColor = new ImgSvgColorizeFilter(0, 0, 0);
159
+ }
160
+ solve() {
161
+ const result = this.solveNarrow(this.solveWide());
162
+ return {
163
+ values: result.values,
164
+ loss: result.loss,
165
+ filter: this.css(result.values),
166
+ };
167
+ }
168
+ solveWide() {
169
+ const A = 5;
170
+ const c = 15;
171
+ const a = [60, 180, 18000, 600, 1.2, 1.2];
172
+ let best = { loss: Infinity, values: null };
173
+ for (let i = 0; best.loss > 25 && i < 3; i++) {
174
+ const initial = [50, 20, 3750, 50, 100, 100];
175
+ const result = this.spsa(A, a, c, initial, 1000);
176
+ if (result.loss < best.loss) {
177
+ best = result;
178
+ }
179
+ }
180
+ return best;
181
+ }
182
+ solveNarrow(wide) {
183
+ const A = wide.loss;
184
+ const c = 2;
185
+ const A1 = A + 1;
186
+ const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
187
+ return this.spsa(A, a, c, wide.values, 500);
188
+ }
189
+ spsa(A, a, c, values, iters) {
190
+ const alpha = 1;
191
+ const gamma = 0.16666666666666666;
192
+ let best = null;
193
+ let bestLoss = Infinity;
194
+ const deltas = new Array(6);
195
+ const highArgs = new Array(6);
196
+ const lowArgs = new Array(6);
197
+ for (let k = 0; k < iters; k++) {
198
+ const ck = c / Math.pow(k + 1, gamma);
199
+ for (let i = 0; i < 6; i++) {
200
+ deltas[i] = Math.random() > 0.5 ? 1 : -1;
201
+ highArgs[i] = values[i] + ck * deltas[i];
202
+ lowArgs[i] = values[i] - ck * deltas[i];
203
+ }
204
+ const lossDiff = this.loss(highArgs) - this.loss(lowArgs);
205
+ for (let i = 0; i < 6; i++) {
206
+ const g = (lossDiff / (2 * ck)) * deltas[i];
207
+ const ak = a[i] / Math.pow(A + k + 1, alpha);
208
+ values[i] = fix(values[i] - ak * g, i);
209
+ }
210
+ const loss = this.loss(values);
211
+ if (loss < bestLoss) {
212
+ best = values.slice(0);
213
+ bestLoss = loss;
214
+ }
215
+ }
216
+ return { values: best, loss: bestLoss };
217
+ function fix(value, idx) {
218
+ let max = 100;
219
+ if (idx === 2 /* saturate */) {
220
+ max = 7500;
221
+ }
222
+ else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) {
223
+ max = 200;
224
+ }
225
+ if (idx === 3 /* hue-rotate */) {
226
+ if (value > max) {
227
+ value %= max;
228
+ }
229
+ else if (value < 0) {
230
+ value = max + (value % max);
231
+ }
232
+ }
233
+ else if (value < 0) {
234
+ value = 0;
235
+ }
236
+ else if (value > max) {
237
+ value = max;
238
+ }
239
+ return value;
240
+ }
241
+ }
242
+ loss(filters) {
243
+ // Argument is array of percentages.
244
+ const color = this.reusedColor;
245
+ color.set(0, 0, 0);
246
+ color.invert(filters[0] / 100);
247
+ color.sepia(filters[1] / 100);
248
+ color.saturate(filters[2] / 100);
249
+ color.hueRotate(filters[3] * 3.6);
250
+ color.brightness(filters[4] / 100);
251
+ color.contrast(filters[5] / 100);
252
+ const colorHSL = color.hsl();
253
+ return (Math.abs(color.r - this.target.r) +
254
+ Math.abs(color.g - this.target.g) +
255
+ Math.abs(color.b - this.target.b) +
256
+ Math.abs(colorHSL.h - this.targetHSL.h) +
257
+ Math.abs(colorHSL.s - this.targetHSL.s) +
258
+ Math.abs(colorHSL.l - this.targetHSL.l));
259
+ }
260
+ css(filters) {
261
+ function fmt(idx, multiplier = 1) {
262
+ return Math.round(filters[idx] * multiplier);
263
+ }
264
+ return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%);`;
265
+ }
266
+ }
267
+ export function hexToRgb(hex) {
268
+ // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
269
+ const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
270
+ hex = hex.replace(shorthandRegex, (m, r, g, b) => {
271
+ return r + r + g + g + b + b;
272
+ });
273
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
274
+ return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null;
275
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reely/colors",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",