splatone 0.0.1
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.
- package/.gitattributes +2 -0
- package/.vscode/settings.json +1 -0
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/crawler.js +570 -0
- package/lib/PluginBase.js +23 -0
- package/lib/VisualizerBase.js +15 -0
- package/lib/paletteGenerator.js +532 -0
- package/lib/pluginLoader.js +146 -0
- package/lib/util.js +180 -0
- package/package.json +36 -0
- package/plugins/flickr/index.js +55 -0
- package/plugins/flickr/worker.js +77 -0
- package/public/style.css +318 -0
- package/public/visualizer.js +49 -0
- package/views/index.ejs +740 -0
- package/visualizer/bulky/node.js +41 -0
- package/visualizer/bulky/web.js +29 -0
- package/visualizer/marker-cluster/node.js +50 -0
- package/visualizer/marker-cluster/public/style.css +1 -0
- package/visualizer/marker-cluster/web.js +153 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import chroma from "chroma-js";
|
|
2
|
+
/**
|
|
3
|
+
chroma.palette-gen.js - a palette generator for data scientists
|
|
4
|
+
based on Chroma.js HCL color space
|
|
5
|
+
Copyright (C) 2016 Mathieu Jacomy
|
|
6
|
+
|
|
7
|
+
The JavaScript code in this page is free software: you can
|
|
8
|
+
redistribute it and/or modify it under the terms of the GNU
|
|
9
|
+
General Public License (GNU GPL) as published by the Free Software
|
|
10
|
+
Foundation, either version 3 of the License, or (at your option)
|
|
11
|
+
any later version. The code is distributed WITHOUT ANY WARRANTY;
|
|
12
|
+
without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
13
|
+
FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
|
|
14
|
+
|
|
15
|
+
As additional permission under GNU GPL version 3 section 7, you
|
|
16
|
+
may distribute non-source (e.g., minimized or compacted) forms of
|
|
17
|
+
that code without the copy of the GNU GPL normally required by
|
|
18
|
+
section 4, provided you include this license notice and a URL
|
|
19
|
+
through which recipients can access the Corresponding Source.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// v0.2
|
|
23
|
+
|
|
24
|
+
Math.random.seed = (function me (s) {
|
|
25
|
+
// Xorshift128 (init seed with Xorshift32)
|
|
26
|
+
s ^= s << 13; s ^= 2 >>> 17; s ^= s << 5;
|
|
27
|
+
let x = 123456789^s;
|
|
28
|
+
s ^= s << 13; s ^= 2 >>> 17; s ^= s << 5;
|
|
29
|
+
let y = 362436069^s;
|
|
30
|
+
s ^= s << 13; s ^= 2 >>> 17; s ^= s << 5;
|
|
31
|
+
let z = 521288629^s;
|
|
32
|
+
s ^= s << 13; s ^= 2 >>> 17; s ^= s << 5;
|
|
33
|
+
let w = 88675123^s;
|
|
34
|
+
let t;
|
|
35
|
+
Math.random = function () {
|
|
36
|
+
t = x ^ (x << 11);
|
|
37
|
+
x = y; y = z; z = w;
|
|
38
|
+
// >>>0 means 'cast to uint32'
|
|
39
|
+
w = ((w ^ (w >>> 19)) ^ (t ^ (t >>> 8)))>>>0;
|
|
40
|
+
return w / 0x100000000;
|
|
41
|
+
};
|
|
42
|
+
Math.random.seed = me;
|
|
43
|
+
return me;
|
|
44
|
+
})(0);
|
|
45
|
+
Math.random.seed(25);
|
|
46
|
+
|
|
47
|
+
const paletteGenerator = (function(undefined){
|
|
48
|
+
const ns = {}
|
|
49
|
+
|
|
50
|
+
ns.generate = function(colorsCount, checkColor, forceMode, quality, ultra_precision, distanceType){
|
|
51
|
+
// Default
|
|
52
|
+
if(colorsCount === undefined)
|
|
53
|
+
colorsCount = 8;
|
|
54
|
+
if(checkColor === undefined)
|
|
55
|
+
checkColor = function(x){return true;};
|
|
56
|
+
if(forceMode === undefined)
|
|
57
|
+
forceMode = false;
|
|
58
|
+
if(quality === undefined)
|
|
59
|
+
quality = 50;
|
|
60
|
+
if(distanceType === undefined)
|
|
61
|
+
distanceType = 'Default';
|
|
62
|
+
ultra_precision = ultra_precision || false
|
|
63
|
+
|
|
64
|
+
console.log('Generate palettes for '+colorsCount+' colors using color distance "'+distanceType+'"')
|
|
65
|
+
|
|
66
|
+
if(forceMode){
|
|
67
|
+
// Force Vector Mode
|
|
68
|
+
|
|
69
|
+
var colors = [];
|
|
70
|
+
|
|
71
|
+
// It will be necessary to check if a Lab color exists in the rgb space.
|
|
72
|
+
function checkLab(lab){
|
|
73
|
+
var color = chroma.lab(lab[0], lab[1], lab[2]);
|
|
74
|
+
return ns.validateLab(lab) && checkColor(color);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Init
|
|
78
|
+
var vectors = {};
|
|
79
|
+
for(let i=0; i<colorsCount; i++){
|
|
80
|
+
// Find a valid Lab color
|
|
81
|
+
var color = [100*Math.random(),100*(2*Math.random()-1),100*(2*Math.random()-1)];
|
|
82
|
+
while(!checkLab(color)){
|
|
83
|
+
color = [100*Math.random(),100*(2*Math.random()-1),100*(2*Math.random()-1)];
|
|
84
|
+
}
|
|
85
|
+
colors.push(color);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Force vector: repulsion
|
|
89
|
+
var repulsion = 100;
|
|
90
|
+
var speed = 100;
|
|
91
|
+
var steps = quality * 20;
|
|
92
|
+
while(steps-- > 0){
|
|
93
|
+
// Init
|
|
94
|
+
for(let i=0; i<colors.length; i++){
|
|
95
|
+
vectors[i] = {dl:0, da:0, db:0};
|
|
96
|
+
}
|
|
97
|
+
// Compute Force
|
|
98
|
+
for(let i=0; i<colors.length; i++){
|
|
99
|
+
var colorA = colors[i];
|
|
100
|
+
for(let j=0; j<i; j++){
|
|
101
|
+
var colorB = colors[j];
|
|
102
|
+
|
|
103
|
+
// repulsion force
|
|
104
|
+
var dl = colorA[0]-colorB[0];
|
|
105
|
+
var da = colorA[1]-colorB[1];
|
|
106
|
+
var db = colorA[2]-colorB[2];
|
|
107
|
+
var d = ns.getColorDistance(colorA, colorB, distanceType)
|
|
108
|
+
if(d>0){
|
|
109
|
+
var force = repulsion/Math.pow(d,2);
|
|
110
|
+
|
|
111
|
+
vectors[i].dl += dl * force / d;
|
|
112
|
+
vectors[i].da += da * force / d;
|
|
113
|
+
vectors[i].db += db * force / d;
|
|
114
|
+
|
|
115
|
+
vectors[j].dl -= dl * force / d;
|
|
116
|
+
vectors[j].da -= da * force / d;
|
|
117
|
+
vectors[j].db -= db * force / d;
|
|
118
|
+
} else {
|
|
119
|
+
// Jitter
|
|
120
|
+
vectors[j].dl += 2 - 4 * Math.random();
|
|
121
|
+
vectors[j].da += 2 - 4 * Math.random();
|
|
122
|
+
vectors[j].db += 2 - 4 * Math.random();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Apply Force
|
|
127
|
+
for(let i=0; i<colors.length; i++){
|
|
128
|
+
var color = colors[i];
|
|
129
|
+
var displacement = speed * Math.sqrt(Math.pow(vectors[i].dl, 2)+Math.pow(vectors[i].da, 2)+Math.pow(vectors[i].db, 2));
|
|
130
|
+
if(displacement>0){
|
|
131
|
+
var ratio = speed * Math.min(0.1, displacement)/displacement;
|
|
132
|
+
const candidateLab = [color[0] + vectors[i].dl*ratio, color[1] + vectors[i].da*ratio, color[2] + vectors[i].db*ratio];
|
|
133
|
+
if(checkLab(candidateLab)){
|
|
134
|
+
colors[i] = candidateLab;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return colors.map(function(lab){return chroma.lab(lab[0], lab[1], lab[2]);});
|
|
140
|
+
|
|
141
|
+
} else {
|
|
142
|
+
|
|
143
|
+
// K-Means Mode
|
|
144
|
+
function checkColor2(lab){
|
|
145
|
+
// Check that a color is valid: it must verify our checkColor condition, but also be in the color space
|
|
146
|
+
var color = chroma.lab(lab);
|
|
147
|
+
var hcl = color.hcl();
|
|
148
|
+
return ns.validateLab(lab) && checkColor(color);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var kMeans = [];
|
|
152
|
+
for(let i=0; i<colorsCount; i++){
|
|
153
|
+
var lab = [100*Math.random(),100*(2*Math.random()-1),100*(2*Math.random()-1)];
|
|
154
|
+
var failsafe=10;
|
|
155
|
+
while(!checkColor2(lab) && failsafe-->0){
|
|
156
|
+
lab = [100*Math.random(),100*(2*Math.random()-1),100*(2*Math.random()-1)];
|
|
157
|
+
}
|
|
158
|
+
kMeans.push(lab);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
var colorSamples = [];
|
|
163
|
+
var samplesClosest = [];
|
|
164
|
+
if(ultra_precision){
|
|
165
|
+
for(let l=0; l<=100; l+=1){
|
|
166
|
+
for(let a=-100; a<=100; a+=5){
|
|
167
|
+
for(let b=-100; b<=100; b+=5){
|
|
168
|
+
if(checkColor2([l, a, b])){
|
|
169
|
+
colorSamples.push([l, a, b]);
|
|
170
|
+
samplesClosest.push(null);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
for(l=0; l<=100; l+=5){
|
|
177
|
+
for(a=-100; a<=100; a+=10){
|
|
178
|
+
for(let b=-100; b<=100; b+=10){
|
|
179
|
+
if(checkColor2([l, a, b])){
|
|
180
|
+
colorSamples.push([l, a, b]);
|
|
181
|
+
samplesClosest.push(null);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Steps
|
|
189
|
+
var steps = quality;
|
|
190
|
+
while(steps-- > 0){
|
|
191
|
+
// kMeans -> Samples Closest
|
|
192
|
+
for(let i=0; i<colorSamples.length; i++){
|
|
193
|
+
var lab = colorSamples[i];
|
|
194
|
+
var minDistance = Infinity;
|
|
195
|
+
for(let j=0; j<kMeans.length; j++){
|
|
196
|
+
var kMean = kMeans[j];
|
|
197
|
+
var distance = ns.getColorDistance(lab, kMean, distanceType);
|
|
198
|
+
if(distance < minDistance){
|
|
199
|
+
minDistance = distance;
|
|
200
|
+
samplesClosest[i] = j;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Samples -> kMeans
|
|
206
|
+
var freeColorSamples = colorSamples.slice(0);
|
|
207
|
+
for(let j=0; j<kMeans.length; j++){
|
|
208
|
+
var count = 0;
|
|
209
|
+
var candidateKMean = [0, 0, 0];
|
|
210
|
+
for(let i=0; i<colorSamples.length; i++){
|
|
211
|
+
if(samplesClosest[i] == j){
|
|
212
|
+
count++;
|
|
213
|
+
candidateKMean[0] += colorSamples[i][0];
|
|
214
|
+
candidateKMean[1] += colorSamples[i][1];
|
|
215
|
+
candidateKMean[2] += colorSamples[i][2];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if(count!=0){
|
|
219
|
+
candidateKMean[0] /= count;
|
|
220
|
+
candidateKMean[1] /= count;
|
|
221
|
+
candidateKMean[2] /= count;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if(count!=0 && checkColor2([candidateKMean[0], candidateKMean[1], candidateKMean[2]]) && candidateKMean){
|
|
225
|
+
kMeans[j] = candidateKMean;
|
|
226
|
+
} else {
|
|
227
|
+
// The candidate kMean is out of the boundaries of the color space, or unfound.
|
|
228
|
+
if(freeColorSamples.length>0){
|
|
229
|
+
// We just search for the closest FREE color of the candidate kMean
|
|
230
|
+
var minDistance = Infinity;
|
|
231
|
+
var closest = -1;
|
|
232
|
+
for(let i=0; i<freeColorSamples.length; i++){
|
|
233
|
+
var distance = ns.getColorDistance(freeColorSamples[i], candidateKMean, distanceType);
|
|
234
|
+
if(distance < minDistance){
|
|
235
|
+
minDistance = distance;
|
|
236
|
+
closest = i;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (closest>=0)
|
|
240
|
+
kMeans[j] = colorSamples[closest];
|
|
241
|
+
|
|
242
|
+
} else {
|
|
243
|
+
// Then we just search for the closest color of the candidate kMean
|
|
244
|
+
var minDistance = Infinity;
|
|
245
|
+
var closest = -1;
|
|
246
|
+
for(let i=0; i<colorSamples.length; i++){
|
|
247
|
+
var distance = ns.getColorDistance(colorSamples[i], candidateKMean, distanceType)
|
|
248
|
+
if(distance < minDistance){
|
|
249
|
+
minDistance = distance;
|
|
250
|
+
closest = i;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (closest>=0)
|
|
254
|
+
kMeans[j] = colorSamples[closest];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
freeColorSamples = freeColorSamples.filter(function(color){
|
|
258
|
+
return color[0] != kMeans[j][0]
|
|
259
|
+
|| color[1] != kMeans[j][1]
|
|
260
|
+
|| color[2] != kMeans[j][2];
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return kMeans.map(function(lab){return chroma.lab(lab[0], lab[1], lab[2]);});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
ns.diffSort = function(colorsToSort, distanceType){
|
|
269
|
+
// Sort
|
|
270
|
+
var diffColors = [colorsToSort.shift()];
|
|
271
|
+
while(colorsToSort.length>0){
|
|
272
|
+
var index = -1;
|
|
273
|
+
var maxDistance = -1;
|
|
274
|
+
for(let candidate_index=0; candidate_index<colorsToSort.length; candidate_index++){
|
|
275
|
+
var d = Infinity;
|
|
276
|
+
for(let i=0; i<diffColors.length; i++){
|
|
277
|
+
var colorA = colorsToSort[candidate_index].lab();
|
|
278
|
+
var colorB = diffColors[i].lab();
|
|
279
|
+
var d = ns.getColorDistance(colorA, colorB, distanceType);
|
|
280
|
+
}
|
|
281
|
+
if(d > maxDistance){
|
|
282
|
+
maxDistance = d;
|
|
283
|
+
index = candidate_index;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
var color = colorsToSort[index];
|
|
287
|
+
diffColors.push(color);
|
|
288
|
+
colorsToSort = colorsToSort.filter(function(c,i){return i!=index;});
|
|
289
|
+
}
|
|
290
|
+
return diffColors;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
ns.getColorDistance = function(lab1, lab2, _type) {
|
|
294
|
+
|
|
295
|
+
var type = _type || 'Default'
|
|
296
|
+
|
|
297
|
+
if (type == 'Default') return _euclidianDistance(lab1, lab2)
|
|
298
|
+
if (type == 'Euclidian') return _euclidianDistance(lab1, lab2)
|
|
299
|
+
if (type == 'CMC') return _cmcDistance(lab1, lab2, 2, 1)
|
|
300
|
+
if (type == 'Compromise') return compromiseDistance(lab1, lab2)
|
|
301
|
+
else return distanceColorblind(lab1, lab2, type)
|
|
302
|
+
|
|
303
|
+
function distanceColorblind(lab1, lab2, type) {
|
|
304
|
+
var lab1_cb = ns.simulate(lab1, type);
|
|
305
|
+
var lab2_cb = ns.simulate(lab2, type);
|
|
306
|
+
return _cmcDistance(lab1_cb, lab2_cb, 2, 1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function compromiseDistance(lab1, lab2) {
|
|
310
|
+
var distances = []
|
|
311
|
+
var coeffs = []
|
|
312
|
+
distances.push(_cmcDistance(lab1, lab2, 2, 1))
|
|
313
|
+
coeffs.push(1000)
|
|
314
|
+
var types = ['Protanope', 'Deuteranope', 'Tritanope']
|
|
315
|
+
types.forEach(function(type){
|
|
316
|
+
var lab1_cb = ns.simulate(lab1, type);
|
|
317
|
+
var lab2_cb = ns.simulate(lab2, type);
|
|
318
|
+
if( !(lab1_cb.some(isNaN) || lab2_cb.some(isNaN)) ) {
|
|
319
|
+
var c
|
|
320
|
+
switch (type) {
|
|
321
|
+
case('Protanope'):
|
|
322
|
+
c = 100;
|
|
323
|
+
break;
|
|
324
|
+
case('Deuteranope'):
|
|
325
|
+
c = 500;
|
|
326
|
+
break;
|
|
327
|
+
case('Tritanope'):
|
|
328
|
+
c = 1;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
distances.push(_cmcDistance(lab1_cb, lab2_cb, 2, 1))
|
|
332
|
+
coeffs.push(c)
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
var total = 0
|
|
336
|
+
var count = 0
|
|
337
|
+
distances.forEach(function(d, i){
|
|
338
|
+
total += coeffs[i] * d
|
|
339
|
+
count += coeffs[i]
|
|
340
|
+
})
|
|
341
|
+
return total / count;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function _euclidianDistance(lab1, lab2) {
|
|
345
|
+
return Math.sqrt(Math.pow(lab1[0]-lab2[0], 2) + Math.pow(lab1[1]-lab2[1], 2) + Math.pow(lab1[2]-lab2[2], 2));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CMC.html
|
|
349
|
+
function _cmcDistance(lab1, lab2, l, c) {
|
|
350
|
+
var L1 = lab1[0]
|
|
351
|
+
var L2 = lab2[0]
|
|
352
|
+
var a1 = lab1[1]
|
|
353
|
+
var a2 = lab2[1]
|
|
354
|
+
var b1 = lab1[2]
|
|
355
|
+
var b2 = lab2[2]
|
|
356
|
+
var C1 = Math.sqrt(Math.pow(a1, 2) + Math.pow(b1, 2))
|
|
357
|
+
var C2 = Math.sqrt(Math.pow(a2, 2) + Math.pow(b2, 2))
|
|
358
|
+
var deltaC = C1 - C2
|
|
359
|
+
var deltaL = L1 - L2
|
|
360
|
+
var deltaa = a1 - a2
|
|
361
|
+
var deltab = b1 - b2
|
|
362
|
+
var deltaH = Math.sqrt(Math.pow(deltaa, 2) + Math.pow(deltab, 2) + Math.pow(deltaC, 2))
|
|
363
|
+
var H1 = Math.atan2(b1, a1) * (180 / Math.PI)
|
|
364
|
+
while (H1 < 0) { H1 += 360 }
|
|
365
|
+
var F = Math.sqrt( Math.pow(C1, 4) / ( Math.pow(C1, 4) + 1900 ) )
|
|
366
|
+
var T = (164 <= H1 && H1 <= 345) ? ( 0.56 + Math.abs(0.2 * Math.cos(H1 + 168)) ) : ( 0.36 + Math.abs(0.4 * Math.cos(H1 + 35)) )
|
|
367
|
+
var S_L = (lab1[0]<16) ? (0.511) : (0.040975 * L1 / (1 + 0.01765 * L1) )
|
|
368
|
+
var S_C = (0.0638 * C1 / (1 + 0.0131 * C1)) + 0.638
|
|
369
|
+
var S_H = S_C * (F*T + 1 - F)
|
|
370
|
+
var result = Math.sqrt( Math.pow(deltaL/(l*S_L), 2) + Math.pow(deltaC/(c*S_C), 2) + Math.pow(deltaH/S_H, 2) )
|
|
371
|
+
return result
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
ns.confusionLines = {
|
|
377
|
+
"Protanope": {
|
|
378
|
+
x: 0.7465,
|
|
379
|
+
y: 0.2535,
|
|
380
|
+
m: 1.273463,
|
|
381
|
+
yint: -0.073894
|
|
382
|
+
},
|
|
383
|
+
"Deuteranope": {
|
|
384
|
+
x: 1.4,
|
|
385
|
+
y: -0.4,
|
|
386
|
+
m: 0.968437,
|
|
387
|
+
yint: 0.003331
|
|
388
|
+
},
|
|
389
|
+
"Tritanope": {
|
|
390
|
+
x: 0.1748,
|
|
391
|
+
y: 0.0,
|
|
392
|
+
m: 0.062921,
|
|
393
|
+
yint: 0.292119
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
ns.simulate_cache = {}
|
|
398
|
+
|
|
399
|
+
ns.simulate = function(lab, type, _amount) {
|
|
400
|
+
// WARNING: may return [NaN, NaN, NaN]
|
|
401
|
+
|
|
402
|
+
var amount = _amount || 1
|
|
403
|
+
|
|
404
|
+
// Cache
|
|
405
|
+
var key = lab.join('-') + '-' + type + '-' + amount
|
|
406
|
+
var cache = ns.simulate_cache[key]
|
|
407
|
+
if (cache) return cache
|
|
408
|
+
|
|
409
|
+
// Get data from type
|
|
410
|
+
var confuse_x = ns.confusionLines[type].x;
|
|
411
|
+
var confuse_y = ns.confusionLines[type].y;
|
|
412
|
+
var confuse_m = ns.confusionLines[type].m;
|
|
413
|
+
var confuse_yint = ns.confusionLines[type].yint;
|
|
414
|
+
|
|
415
|
+
// Code adapted from http://galacticmilk.com/labs/Color-Vision/Javascript/Color.Vision.Simulate.js
|
|
416
|
+
var color = chroma.lab(lab[0], lab[1], lab[2]);
|
|
417
|
+
var sr = color.rgb()[0];
|
|
418
|
+
var sg = color.rgb()[1];
|
|
419
|
+
var sb = color.rgb()[2];
|
|
420
|
+
var dr = sr; // destination color
|
|
421
|
+
var dg = sg;
|
|
422
|
+
var db = sb;
|
|
423
|
+
// Convert source color into XYZ color space
|
|
424
|
+
var pow_r = Math.pow(sr, 2.2);
|
|
425
|
+
var pow_g = Math.pow(sg, 2.2);
|
|
426
|
+
var pow_b = Math.pow(sb, 2.2);
|
|
427
|
+
var X = pow_r * 0.412424 + pow_g * 0.357579 + pow_b * 0.180464; // RGB->XYZ (sRGB:D65)
|
|
428
|
+
var Y = pow_r * 0.212656 + pow_g * 0.715158 + pow_b * 0.0721856;
|
|
429
|
+
var Z = pow_r * 0.0193324 + pow_g * 0.119193 + pow_b * 0.950444;
|
|
430
|
+
// Convert XYZ into xyY Chromacity Coordinates (xy) and Luminance (Y)
|
|
431
|
+
var chroma_x = X / (X + Y + Z);
|
|
432
|
+
var chroma_y = Y / (X + Y + Z);
|
|
433
|
+
// Generate the "Confusion Line" between the source color and the Confusion Point
|
|
434
|
+
var m = (chroma_y - confuse_y) / (chroma_x - confuse_x); // slope of Confusion Line
|
|
435
|
+
var yint = chroma_y - chroma_x * m; // y-intercept of confusion line (x-intercept = 0.0)
|
|
436
|
+
// How far the xy coords deviate from the simulation
|
|
437
|
+
var deviate_x = (confuse_yint - yint) / (m - confuse_m);
|
|
438
|
+
var deviate_y = (m * deviate_x) + yint;
|
|
439
|
+
// Compute the simulated color's XYZ coords
|
|
440
|
+
var X = deviate_x * Y / deviate_y;
|
|
441
|
+
var Z = (1.0 - (deviate_x + deviate_y)) * Y / deviate_y;
|
|
442
|
+
// Neutral grey calculated from luminance (in D65)
|
|
443
|
+
var neutral_X = 0.312713 * Y / 0.329016;
|
|
444
|
+
var neutral_Z = 0.358271 * Y / 0.329016;
|
|
445
|
+
// Difference between simulated color and neutral grey
|
|
446
|
+
var diff_X = neutral_X - X;
|
|
447
|
+
var diff_Z = neutral_Z - Z;
|
|
448
|
+
diff_r = diff_X * 3.24071 + diff_Z * -0.498571; // XYZ->RGB (sRGB:D65)
|
|
449
|
+
diff_g = diff_X * -0.969258 + diff_Z * 0.0415557;
|
|
450
|
+
diff_b = diff_X * 0.0556352 + diff_Z * 1.05707;
|
|
451
|
+
// Convert to RGB color space
|
|
452
|
+
dr = X * 3.24071 + Y * -1.53726 + Z * -0.498571; // XYZ->RGB (sRGB:D65)
|
|
453
|
+
dg = X * -0.969258 + Y * 1.87599 + Z * 0.0415557;
|
|
454
|
+
db = X * 0.0556352 + Y * -0.203996 + Z * 1.05707;
|
|
455
|
+
// Compensate simulated color towards a neutral fit in RGB space
|
|
456
|
+
var fit_r = ((dr < 0.0 ? 0.0 : 1.0) - dr) / diff_r;
|
|
457
|
+
var fit_g = ((dg < 0.0 ? 0.0 : 1.0) - dg) / diff_g;
|
|
458
|
+
var fit_b = ((db < 0.0 ? 0.0 : 1.0) - db) / diff_b;
|
|
459
|
+
var adjust = Math.max( // highest value
|
|
460
|
+
(fit_r > 1.0 || fit_r < 0.0) ? 0.0 : fit_r,
|
|
461
|
+
(fit_g > 1.0 || fit_g < 0.0) ? 0.0 : fit_g,
|
|
462
|
+
(fit_b > 1.0 || fit_b < 0.0) ? 0.0 : fit_b
|
|
463
|
+
);
|
|
464
|
+
// Shift proportional to the greatest shift
|
|
465
|
+
dr = dr + (adjust * diff_r);
|
|
466
|
+
dg = dg + (adjust * diff_g);
|
|
467
|
+
db = db + (adjust * diff_b);
|
|
468
|
+
// Apply gamma correction
|
|
469
|
+
dr = Math.pow(dr, 1.0 / 2.2);
|
|
470
|
+
dg = Math.pow(dg, 1.0 / 2.2);
|
|
471
|
+
db = Math.pow(db, 1.0 / 2.2);
|
|
472
|
+
// Anomylize colors
|
|
473
|
+
dr = sr * (1.0 - amount) + dr * amount;
|
|
474
|
+
dg = sg * (1.0 - amount) + dg * amount;
|
|
475
|
+
db = sb * (1.0 - amount) + db * amount;
|
|
476
|
+
var dcolor = chroma.rgb(dr, dg, db);
|
|
477
|
+
var result = dcolor.lab()
|
|
478
|
+
ns.simulate_cache[key] = result
|
|
479
|
+
return result
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
ns.validateLab = function(lab) {
|
|
483
|
+
// Code from Chroma.js 2016
|
|
484
|
+
|
|
485
|
+
var LAB_CONSTANTS = {
|
|
486
|
+
// Corresponds roughly to RGB brighter/darker
|
|
487
|
+
Kn: 18,
|
|
488
|
+
|
|
489
|
+
// D65 standard referent
|
|
490
|
+
Xn: 0.950470,
|
|
491
|
+
Yn: 1,
|
|
492
|
+
Zn: 1.088830,
|
|
493
|
+
|
|
494
|
+
t0: 0.137931034, // 4 / 29
|
|
495
|
+
t1: 0.206896552, // 6 / 29
|
|
496
|
+
t2: 0.12841855, // 3 * t1 * t1
|
|
497
|
+
t3: 0.008856452 // t1 * t1 * t1
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
var l = lab[0]
|
|
501
|
+
var a = lab[1]
|
|
502
|
+
var b = lab[2]
|
|
503
|
+
|
|
504
|
+
var y = (l + 16) / 116
|
|
505
|
+
var x = (isNaN(a)) ? (y) : (y + a / 500)
|
|
506
|
+
var z = (isNaN(b)) ? (y) : (y - b / 200)
|
|
507
|
+
|
|
508
|
+
y = LAB_CONSTANTS.Yn * lab_xyz(y)
|
|
509
|
+
x = LAB_CONSTANTS.Xn * lab_xyz(x)
|
|
510
|
+
z = LAB_CONSTANTS.Zn * lab_xyz(z)
|
|
511
|
+
|
|
512
|
+
var r = xyz_rgb( 3.2404542 * x - 1.5371385 * y - 0.4985314 * z) // D65 -> sRGB
|
|
513
|
+
var g = xyz_rgb( -0.9692660 * x + 1.8760108 * y + 0.0415560 * z)
|
|
514
|
+
var b = xyz_rgb( 0.0556434 * x - 0.2040259 * y + 1.0572252 * z)
|
|
515
|
+
|
|
516
|
+
return r >= 0 && r <= 255
|
|
517
|
+
&& g >= 0 && g <= 255
|
|
518
|
+
&& b >= 0 && b <= 255
|
|
519
|
+
|
|
520
|
+
function xyz_rgb(r) {
|
|
521
|
+
return Math.round(255 * ( (r <= 0.00304) ? (12.92 * r) : (1.055 * Math.pow(r, 1 / 2.4) - 0.055) ) )
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function lab_xyz(t) {
|
|
525
|
+
return (t > LAB_CONSTANTS.t1) ? (t * t * t) : ( LAB_CONSTANTS.t2 * (t - LAB_CONSTANTS.t0) )
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return ns
|
|
530
|
+
})();
|
|
531
|
+
|
|
532
|
+
export default paletteGenerator;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// pluginLoader.js
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export async function loadPlugins({
|
|
7
|
+
dir = path.resolve('plugins'),
|
|
8
|
+
api = {},
|
|
9
|
+
optionsById = {}, // { hello: { ... }, greeter: { ... } }
|
|
10
|
+
filter = () => true, // 例: name => allow.includes(name)
|
|
11
|
+
} = {}) {
|
|
12
|
+
const ctx = {
|
|
13
|
+
log: console.log,
|
|
14
|
+
...api,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// 先に候補フォルダを列挙
|
|
18
|
+
await fs.mkdir(dir, { recursive: true });
|
|
19
|
+
const folders = (await fs.readdir(dir, { withFileTypes: true }))
|
|
20
|
+
.filter(d => d.isDirectory())
|
|
21
|
+
.map(d => d.name)
|
|
22
|
+
.filter(filter);
|
|
23
|
+
|
|
24
|
+
// 1) メタ読み(静的プロパティを得るため一旦 import)
|
|
25
|
+
const metas = [];
|
|
26
|
+
for (const folder of folders) {
|
|
27
|
+
const entry = await resolveEntry(path.join(dir, folder));
|
|
28
|
+
if (!entry) { ctx.log(`[plugin] skip (no entry): ${folder}`); continue; }
|
|
29
|
+
const mod = await import(pathToFileURL(entry).href);
|
|
30
|
+
const PluginClass = mod?.default ?? mod;
|
|
31
|
+
validateClass(PluginClass, entry);
|
|
32
|
+
const id = PluginClass.id || folder;
|
|
33
|
+
metas.push({
|
|
34
|
+
id,
|
|
35
|
+
folder,
|
|
36
|
+
entry,
|
|
37
|
+
PluginClass,
|
|
38
|
+
deps: Array.isArray(PluginClass.dependencies) ? PluginClass.dependencies : [],
|
|
39
|
+
version: PluginClass.version || '0.0.0',
|
|
40
|
+
name: PluginClass.name || id,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 2) トポロジカルソート(依存を考慮した読み込み順)
|
|
45
|
+
const order = topoSort(metas.map(m => ({ id: m.id, deps: m.deps })));
|
|
46
|
+
const byId = new Map(metas.map(m => [m.id, m]));
|
|
47
|
+
|
|
48
|
+
// 3) 生成→init→start
|
|
49
|
+
const instances = new Map(); // id -> instance
|
|
50
|
+
const getPlugin = id => instances.get(id);
|
|
51
|
+
for (const id of order) {
|
|
52
|
+
const meta = byId.get(id);
|
|
53
|
+
if (!meta) {
|
|
54
|
+
// 依存にあるが存在しない
|
|
55
|
+
throw new Error(`[plugin] missing dependency: ${id}`);
|
|
56
|
+
}
|
|
57
|
+
// 依存がすべて存在しているか確認
|
|
58
|
+
for (const dep of meta.deps) {
|
|
59
|
+
if (!instances.has(dep)) throw new Error(`[plugin] unmet dependency "${dep}" for "${id}"`);
|
|
60
|
+
}
|
|
61
|
+
const opts = optionsById[id] || {};
|
|
62
|
+
const instance = new meta.PluginClass({ ...ctx, getPlugin }, opts);
|
|
63
|
+
|
|
64
|
+
await instance.init?.();
|
|
65
|
+
//await instance.start?.();
|
|
66
|
+
|
|
67
|
+
instances.set(id, instance);
|
|
68
|
+
ctx.log(`[plugin] loaded: ${id}@${meta.version}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4) ファサード
|
|
72
|
+
return {
|
|
73
|
+
list() { return [...instances.keys()]; },
|
|
74
|
+
get(id) { return instances.get(id); },
|
|
75
|
+
has(id) { return instances.has(id); },
|
|
76
|
+
/** 任意メソッドを呼ぶ */
|
|
77
|
+
call(id, method, ...args) {
|
|
78
|
+
const p = instances.get(id);
|
|
79
|
+
if (!p) throw new Error(`plugin "${id}" not loaded`);
|
|
80
|
+
const fn = p[method];
|
|
81
|
+
if (typeof fn !== 'function') throw new Error(`method "${method}" not found in plugin "${id}"`);
|
|
82
|
+
return fn.apply(p, args);
|
|
83
|
+
},
|
|
84
|
+
/** 終了時 */
|
|
85
|
+
async stopAll() {
|
|
86
|
+
for (const p of instances.values()) {
|
|
87
|
+
try { await p.stop?.(); } catch (e) { ctx.log('[plugin] stop error', e); }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function resolveEntry(folder) {
|
|
94
|
+
const candidates = ['index.js', 'index.mjs'];
|
|
95
|
+
for (const c of candidates) {
|
|
96
|
+
const f = path.join(folder, c);
|
|
97
|
+
try { await fs.access(f); return f; } catch {}
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const pkg = JSON.parse(await fs.readFile(path.join(folder, 'package.json'), 'utf8'));
|
|
101
|
+
const entry = pkg.module || pkg.main;
|
|
102
|
+
if (entry) return path.join(folder, entry);
|
|
103
|
+
} catch {}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateClass(PluginClass, file) {
|
|
108
|
+
if (typeof PluginClass !== 'function') throw new Error(`Plugin must export a class: ${file}`);
|
|
109
|
+
// 静的メタ
|
|
110
|
+
if (!PluginClass.id || typeof PluginClass.id !== 'string') {
|
|
111
|
+
throw new Error(`static id (string) is required: ${file}`);
|
|
112
|
+
}
|
|
113
|
+
if (!PluginClass.version || typeof PluginClass.version !== 'string') {
|
|
114
|
+
throw new Error(`static version (string) is required: ${file}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function topoSort(nodes) {
|
|
119
|
+
// nodes: [{id, deps:[]}]
|
|
120
|
+
const incoming = new Map(nodes.map(n => [n.id, new Set(n.deps)]));
|
|
121
|
+
const byId = new Map(nodes.map(n => [n.id, n]));
|
|
122
|
+
const res = [];
|
|
123
|
+
const q = [...nodes.filter(n => n.deps.length === 0).map(n => n.id)];
|
|
124
|
+
|
|
125
|
+
// 依存に存在しないIDがあっても最終的に検出できる
|
|
126
|
+
while (q.length) {
|
|
127
|
+
const id = q.shift();
|
|
128
|
+
res.push(id);
|
|
129
|
+
for (const [k, deps] of incoming) {
|
|
130
|
+
if (deps.has(id)) {
|
|
131
|
+
deps.delete(id);
|
|
132
|
+
if (deps.size === 0) q.push(k);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// 未解決(循環 or 不明依存)
|
|
137
|
+
const remaining = [...incoming.entries()].filter(([, s]) => s.size > 0).map(([k]) => k);
|
|
138
|
+
if (remaining.length) {
|
|
139
|
+
const detail = remaining.map(k => `${k}<-${[...incoming.get(k)].join(',')}`).join(' ; ');
|
|
140
|
+
throw new Error(`circular or missing dependencies: ${detail}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 依存なしノードで未訪問があれば(単独で push)
|
|
144
|
+
for (const n of nodes) if (!res.includes(n.id)) res.push(n.id);
|
|
145
|
+
return res;
|
|
146
|
+
}
|