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.
@@ -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
+ }