@newtonedev/colors 0.0.1 → 1.1.0
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/README.md +22 -23
- package/dist/__tests__/key-color.test.d.ts +2 -0
- package/dist/__tests__/key-color.test.d.ts.map +1 -0
- package/dist/config.d.ts +31 -11
- package/dist/config.d.ts.map +1 -1
- package/dist/contrast/apca.d.ts +16 -0
- package/dist/contrast/apca.d.ts.map +1 -1
- package/dist/gamut/max-chroma.d.ts +8 -3
- package/dist/gamut/max-chroma.d.ts.map +1 -1
- package/dist/index.cjs +135 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +125 -76
- package/dist/index.js.map +1 -1
- package/dist/scale/generate.d.ts +44 -34
- package/dist/scale/generate.d.ts.map +1 -1
- package/dist/scale/hue-grade.d.ts +47 -29
- package/dist/scale/hue-grade.d.ts.map +1 -1
- package/dist/scale/key-color.d.ts +31 -27
- package/dist/scale/key-color.d.ts.map +1 -1
- package/dist/scale/resolve-color.d.ts +6 -26
- package/dist/scale/resolve-color.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -302,6 +302,13 @@ function contrastTextHex(background) {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
// src/contrast/apca.ts
|
|
305
|
+
var APCA_LC_MAX = 108;
|
|
306
|
+
function apcaToNormalized(lc) {
|
|
307
|
+
return Math.max(-1, Math.min(1, lc / APCA_LC_MAX));
|
|
308
|
+
}
|
|
309
|
+
function normalizedToApca(normalized) {
|
|
310
|
+
return normalized * APCA_LC_MAX;
|
|
311
|
+
}
|
|
305
312
|
function apcaContrast(textColor, bgColor) {
|
|
306
313
|
let txtY = APCA_SRGB_R * textColor.r ** APCA_MAIN_TRC + APCA_SRGB_G * textColor.g ** APCA_MAIN_TRC + APCA_SRGB_B * textColor.b ** APCA_MAIN_TRC;
|
|
307
314
|
let bgY = APCA_SRGB_R * bgColor.r ** APCA_MAIN_TRC + APCA_SRGB_G * bgColor.g ** APCA_MAIN_TRC + APCA_SRGB_B * bgColor.b ** APCA_MAIN_TRC;
|
|
@@ -343,84 +350,147 @@ function mix(a, b, t) {
|
|
|
343
350
|
}
|
|
344
351
|
|
|
345
352
|
// src/config.ts
|
|
353
|
+
var DEFAULT_SCALE_STEPS = 26;
|
|
354
|
+
var DEFAULT_HUE = 0;
|
|
346
355
|
var MIN_LIGHTEST_L = 0.96;
|
|
347
356
|
var MAX_DARKEST_L = 0.16;
|
|
348
|
-
var
|
|
349
|
-
var
|
|
350
|
-
var
|
|
351
|
-
var
|
|
357
|
+
var GRADING_REACH = 3 / 5;
|
|
358
|
+
var MAX_GRADING_AMOUNT = 0.25;
|
|
359
|
+
var GRADING_CHROMA_RATIO = 0.5;
|
|
360
|
+
var SHIFT_REACH = 2 / 3;
|
|
361
|
+
var MAX_SHIFT_AMOUNT = 0.5;
|
|
352
362
|
var PERCEPTUAL_JND = 0.02;
|
|
353
363
|
|
|
354
364
|
// src/scale/hue-grade.ts
|
|
355
|
-
function gradeHue(baseHue, t, grade, reach =
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
const di = Math.max(0, Math.min(maxIntensity, darkValue));
|
|
365
|
+
function gradeHue(baseHue, t, grade, reach = GRADING_REACH, maxIntensity = MAX_GRADING_AMOUNT) {
|
|
366
|
+
const li = Math.max(0, Math.min(maxIntensity, grade.light?.amount ?? 0));
|
|
367
|
+
const di = Math.max(0, Math.min(maxIntensity, grade.dark?.amount ?? 0));
|
|
359
368
|
if (li === 0 && di === 0) return baseHue;
|
|
360
369
|
const lightInfluence = t <= reach ? 0.5 * (1 + Math.cos(Math.PI * t / reach)) : 0;
|
|
361
370
|
const darkInfluence = t >= 1 - reach ? 0.5 * (1 + Math.cos(Math.PI * (1 - t) / reach)) : 0;
|
|
362
|
-
const
|
|
363
|
-
const baseRad = baseHue *
|
|
371
|
+
const toRad2 = Math.PI / 180;
|
|
372
|
+
const baseRad = baseHue * toRad2;
|
|
364
373
|
const bx = Math.cos(baseRad);
|
|
365
374
|
const by = Math.sin(baseRad);
|
|
366
375
|
let dx = 0;
|
|
367
376
|
let dy = 0;
|
|
368
377
|
if (lightInfluence > 0 && li > 0) {
|
|
369
378
|
const blend = lightInfluence * li;
|
|
370
|
-
const lRad =
|
|
379
|
+
const lRad = grade.light.hue * toRad2;
|
|
371
380
|
dx += blend * (Math.cos(lRad) - bx);
|
|
372
381
|
dy += blend * (Math.sin(lRad) - by);
|
|
373
382
|
}
|
|
374
383
|
if (darkInfluence > 0 && di > 0) {
|
|
375
384
|
const blend = darkInfluence * di;
|
|
376
|
-
const dRad =
|
|
385
|
+
const dRad = grade.dark.hue * toRad2;
|
|
377
386
|
dx += blend * (Math.cos(dRad) - bx);
|
|
378
387
|
dy += blend * (Math.sin(dRad) - by);
|
|
379
388
|
}
|
|
380
389
|
const rx = bx + dx;
|
|
381
390
|
const ry = by + dy;
|
|
382
391
|
if (rx * rx + ry * ry < 1e-20) return baseHue;
|
|
383
|
-
return (Math.atan2(ry, rx) /
|
|
392
|
+
return (Math.atan2(ry, rx) / toRad2 + 360) % 360;
|
|
393
|
+
}
|
|
394
|
+
function gradingInfluence(t, grading) {
|
|
395
|
+
const li = Math.max(0, Math.min(MAX_GRADING_AMOUNT, grading.light?.amount ?? 0));
|
|
396
|
+
const di = Math.max(0, Math.min(MAX_GRADING_AMOUNT, grading.dark?.amount ?? 0));
|
|
397
|
+
if (li === 0 && di === 0) return 0;
|
|
398
|
+
const lightInfluence = t <= GRADING_REACH ? 0.5 * (1 + Math.cos(Math.PI * t / GRADING_REACH)) : 0;
|
|
399
|
+
const darkInfluence = t >= 1 - GRADING_REACH ? 0.5 * (1 + Math.cos(Math.PI * (1 - t) / GRADING_REACH)) : 0;
|
|
400
|
+
const lightBlend = lightInfluence * (li / MAX_GRADING_AMOUNT);
|
|
401
|
+
const darkBlend = darkInfluence * (di / MAX_GRADING_AMOUNT);
|
|
402
|
+
return Math.max(lightBlend, darkBlend);
|
|
384
403
|
}
|
|
385
404
|
function resolveGradedHue(baseHue, t, globalGrade, localGrade) {
|
|
386
405
|
let h = baseHue;
|
|
406
|
+
if (localGrade) h = gradeHue(h, t, localGrade, SHIFT_REACH, MAX_SHIFT_AMOUNT);
|
|
387
407
|
if (globalGrade) h = gradeHue(h, t, globalGrade);
|
|
388
|
-
if (localGrade) h = gradeHue(h, t, localGrade, LOCAL_GRADE_REACH, MAX_LOCAL_GRADE_INTENSITY);
|
|
389
408
|
return h;
|
|
390
409
|
}
|
|
391
|
-
function buildOneSidedGrade(hue,
|
|
392
|
-
return
|
|
410
|
+
function buildOneSidedGrade(hue, amount, light = false) {
|
|
411
|
+
return light ? { light: { hue, amount } } : { dark: { hue, amount } };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/scale/dynamic-range.ts
|
|
415
|
+
function resolveLightest(slider) {
|
|
416
|
+
const s = Math.max(0, Math.min(1, slider));
|
|
417
|
+
return MIN_LIGHTEST_L + s * (1 - MIN_LIGHTEST_L);
|
|
418
|
+
}
|
|
419
|
+
function resolveDarkest(slider) {
|
|
420
|
+
const s = Math.max(0, Math.min(1, slider));
|
|
421
|
+
return MAX_DARKEST_L * (1 - s);
|
|
422
|
+
}
|
|
423
|
+
function lightnessToScaleT(L, lightestL, darkestL) {
|
|
424
|
+
const range = lightestL - darkestL;
|
|
425
|
+
if (range <= 0) return 0.5;
|
|
426
|
+
return Math.max(0, Math.min(1, (lightestL - L) / range));
|
|
393
427
|
}
|
|
394
428
|
|
|
395
429
|
// src/scale/generate.ts
|
|
396
|
-
|
|
430
|
+
var toRad = Math.PI / 180;
|
|
431
|
+
function applyGradingOverlay(C, h, L, t, grading, gamut) {
|
|
432
|
+
const li = Math.max(0, Math.min(MAX_GRADING_AMOUNT, grading.light?.amount ?? 0));
|
|
433
|
+
const di = Math.max(0, Math.min(MAX_GRADING_AMOUNT, grading.dark?.amount ?? 0));
|
|
434
|
+
if (li === 0 && di === 0) return { C, h };
|
|
435
|
+
const lightFade = t <= GRADING_REACH ? 0.5 * (1 + Math.cos(Math.PI * t / GRADING_REACH)) : 0;
|
|
436
|
+
const darkFade = t >= 1 - GRADING_REACH ? 0.5 * (1 + Math.cos(Math.PI * (1 - t) / GRADING_REACH)) : 0;
|
|
437
|
+
const lightBlend = lightFade * (li / MAX_GRADING_AMOUNT);
|
|
438
|
+
const darkBlend = darkFade * (di / MAX_GRADING_AMOUNT);
|
|
439
|
+
if (lightBlend === 0 && darkBlend === 0) return { C, h };
|
|
440
|
+
const hRad = h * toRad;
|
|
441
|
+
let a = C * Math.cos(hRad);
|
|
442
|
+
let b = C * Math.sin(hRad);
|
|
443
|
+
if (lightBlend > 0) {
|
|
444
|
+
const lh = grading.light.hue;
|
|
445
|
+
const lRad = lh * toRad;
|
|
446
|
+
const lC = maxChroma(L, lh, gamut) * GRADING_CHROMA_RATIO * lightBlend;
|
|
447
|
+
a += lC * Math.cos(lRad);
|
|
448
|
+
b += lC * Math.sin(lRad);
|
|
449
|
+
}
|
|
450
|
+
if (darkBlend > 0) {
|
|
451
|
+
const dh = grading.dark.hue;
|
|
452
|
+
const dRad = dh * toRad;
|
|
453
|
+
const dC = maxChroma(L, dh, gamut) * GRADING_CHROMA_RATIO * darkBlend;
|
|
454
|
+
a += dC * Math.cos(dRad);
|
|
455
|
+
b += dC * Math.sin(dRad);
|
|
456
|
+
}
|
|
457
|
+
let newC = Math.sqrt(a * a + b * b);
|
|
458
|
+
let newH = (Math.atan2(b, a) / toRad + 360) % 360;
|
|
459
|
+
newC = Math.min(newC, maxChroma(L, newH, gamut));
|
|
460
|
+
return { C: newC, h: newH };
|
|
461
|
+
}
|
|
462
|
+
function generateScale(options = {}) {
|
|
397
463
|
const {
|
|
398
|
-
|
|
399
|
-
|
|
464
|
+
contrast,
|
|
465
|
+
hue = DEFAULT_HUE,
|
|
400
466
|
chroma,
|
|
401
|
-
|
|
402
|
-
lightest = 1,
|
|
403
|
-
darkest = 0,
|
|
467
|
+
isP3 = false,
|
|
404
468
|
grading,
|
|
405
|
-
|
|
469
|
+
shift
|
|
406
470
|
} = options;
|
|
407
|
-
const
|
|
408
|
-
const
|
|
471
|
+
const gamut = isP3 ? "display-p3" : "srgb";
|
|
472
|
+
const steps = DEFAULT_SCALE_STEPS;
|
|
473
|
+
const chromaRatio = chroma?.amount ?? 0;
|
|
474
|
+
const chromaPeak = chroma?.balance ?? 0.5;
|
|
409
475
|
const hueGrade = grading;
|
|
410
|
-
const localHueGrade =
|
|
411
|
-
if (steps < 2) return [];
|
|
476
|
+
const localHueGrade = shift ? buildOneSidedGrade(shift.hue, shift.amount, shift.light) : void 0;
|
|
412
477
|
const ratio = Math.max(0, Math.min(1, chromaRatio));
|
|
413
478
|
const peak = Math.max(0, Math.min(1, chromaPeak));
|
|
414
|
-
const lightestL =
|
|
415
|
-
const darkestL =
|
|
479
|
+
const lightestL = resolveLightest(contrast?.light ?? 1);
|
|
480
|
+
const darkestL = resolveDarkest(contrast?.dark ?? 1);
|
|
416
481
|
const hueAt = (t) => resolveGradedHue(hue, t, hueGrade, localHueGrade);
|
|
417
482
|
if (peak === 0.5 || ratio === 0 || ratio >= 1) {
|
|
418
483
|
const scale2 = [];
|
|
419
484
|
for (let i = 0; i < steps; i++) {
|
|
420
485
|
const t = i / (steps - 1);
|
|
421
486
|
const L = lightestL - t * (lightestL - darkestL);
|
|
422
|
-
|
|
423
|
-
|
|
487
|
+
let h = hueAt(t);
|
|
488
|
+
let C = Math.min(maxChroma(L, hue, gamut) * ratio, maxChroma(L, h, gamut));
|
|
489
|
+
if (hueGrade) {
|
|
490
|
+
const overlay = applyGradingOverlay(C, h, L, t, hueGrade, gamut);
|
|
491
|
+
C = overlay.C;
|
|
492
|
+
h = overlay.h;
|
|
493
|
+
}
|
|
424
494
|
scale2.push({ L, C, h });
|
|
425
495
|
}
|
|
426
496
|
return scale2;
|
|
@@ -432,7 +502,7 @@ function generateScale(options) {
|
|
|
432
502
|
for (let i = 0; i <= N; i++) {
|
|
433
503
|
const t = i / N;
|
|
434
504
|
const L = lightestL - t * (lightestL - darkestL);
|
|
435
|
-
const C = maxChroma(L,
|
|
505
|
+
const C = maxChroma(L, hue, gamut);
|
|
436
506
|
boundarySamples.push({ t, C });
|
|
437
507
|
if (C > peakBoundaryC) {
|
|
438
508
|
peakBoundaryC = C;
|
|
@@ -461,7 +531,7 @@ function generateScale(options) {
|
|
|
461
531
|
for (let i = 0; i < steps; i++) {
|
|
462
532
|
const t = i / (steps - 1);
|
|
463
533
|
const L = lightestL - t * (lightestL - darkestL);
|
|
464
|
-
|
|
534
|
+
let h = hueAt(t);
|
|
465
535
|
let tWarped;
|
|
466
536
|
if (t <= targetT) {
|
|
467
537
|
tWarped = t * (peakT / targetT);
|
|
@@ -469,16 +539,22 @@ function generateScale(options) {
|
|
|
469
539
|
tWarped = peakT + (t - targetT) * ((1 - peakT) / (1 - targetT));
|
|
470
540
|
}
|
|
471
541
|
const Lwarped = lightestL - tWarped * (lightestL - darkestL);
|
|
472
|
-
const warpedC = maxChroma(Lwarped,
|
|
542
|
+
const warpedC = maxChroma(Lwarped, hue, gamut) * ratio;
|
|
473
543
|
const boundaryC = maxChroma(L, h, gamut);
|
|
474
|
-
|
|
544
|
+
let C = Math.min(warpedC, boundaryC);
|
|
545
|
+
if (hueGrade) {
|
|
546
|
+
const overlay = applyGradingOverlay(C, h, L, t, hueGrade, gamut);
|
|
547
|
+
C = overlay.C;
|
|
548
|
+
h = overlay.h;
|
|
549
|
+
}
|
|
475
550
|
scale.push({ L, C, h });
|
|
476
551
|
}
|
|
477
552
|
return scale;
|
|
478
553
|
}
|
|
479
554
|
|
|
480
555
|
// src/scale/resolve-color.ts
|
|
481
|
-
function resolveColor(input,
|
|
556
|
+
function resolveColor(input, isP3 = false) {
|
|
557
|
+
const gamut = isP3 ? "display-p3" : "srgb";
|
|
482
558
|
const isHex = typeof input === "string";
|
|
483
559
|
const original = isHex ? hexToOklch(input) : input;
|
|
484
560
|
let inGamut;
|
|
@@ -501,53 +577,26 @@ function resolveColor(input, gamut = "srgb") {
|
|
|
501
577
|
chromaRatio: Math.min(1, chromaRatio)
|
|
502
578
|
};
|
|
503
579
|
}
|
|
504
|
-
function findNearest(target, scale) {
|
|
505
|
-
if (scale.length === 0) {
|
|
506
|
-
throw new Error("findNearest: scale must not be empty");
|
|
507
|
-
}
|
|
508
|
-
let bestIndex = 0;
|
|
509
|
-
let bestDistance = Infinity;
|
|
510
|
-
for (let i = 0; i < scale.length; i++) {
|
|
511
|
-
const d = deltaEOK(target, scale[i]);
|
|
512
|
-
if (d < bestDistance) {
|
|
513
|
-
bestDistance = d;
|
|
514
|
-
bestIndex = i;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return {
|
|
518
|
-
index: bestIndex,
|
|
519
|
-
color: scale[bestIndex],
|
|
520
|
-
distance: bestDistance
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// src/scale/dynamic-range.ts
|
|
525
|
-
function resolveLightest(slider) {
|
|
526
|
-
const s = Math.max(0, Math.min(1, slider));
|
|
527
|
-
return MIN_LIGHTEST_L + s * (1 - MIN_LIGHTEST_L);
|
|
528
|
-
}
|
|
529
|
-
function resolveDarkest(slider) {
|
|
530
|
-
const s = Math.max(0, Math.min(1, slider));
|
|
531
|
-
return MAX_DARKEST_L * (1 - s);
|
|
532
|
-
}
|
|
533
|
-
function lightnessToScaleT(L, lightestL, darkestL) {
|
|
534
|
-
const range = lightestL - darkestL;
|
|
535
|
-
if (range <= 0) return 0.5;
|
|
536
|
-
return Math.max(0, Math.min(1, (lightestL - L) / range));
|
|
537
|
-
}
|
|
538
580
|
|
|
539
581
|
// src/scale/key-color.ts
|
|
540
582
|
function keyColor(color, options) {
|
|
541
|
-
const {
|
|
542
|
-
const resolved = resolveColor(color,
|
|
543
|
-
const
|
|
583
|
+
const { isP3 = false, contrast, steps = DEFAULT_SCALE_STEPS } = options ?? {};
|
|
584
|
+
const resolved = resolveColor(color, isP3);
|
|
585
|
+
const lightestL = resolveLightest(contrast?.light ?? 1);
|
|
586
|
+
const darkestL = resolveDarkest(contrast?.dark ?? 1);
|
|
587
|
+
const balance = lightnessToScaleT(resolved.oklch.L, lightestL, darkestL);
|
|
588
|
+
const stepIndex = Math.round(balance * Math.max(0, steps - 1));
|
|
544
589
|
return {
|
|
545
590
|
hue: resolved.hue,
|
|
546
|
-
chroma: {
|
|
547
|
-
|
|
591
|
+
chroma: { amount: resolved.chromaRatio, balance },
|
|
592
|
+
stepIndex,
|
|
593
|
+
hex: resolved.hex,
|
|
594
|
+
oklch: resolved.oklch,
|
|
595
|
+
wasRemapped: resolved.wasRemapped,
|
|
596
|
+
original: resolved.original
|
|
548
597
|
};
|
|
549
598
|
}
|
|
550
599
|
|
|
551
|
-
export {
|
|
600
|
+
export { APCA_LC_MAX, DEFAULT_HUE, DEFAULT_SCALE_STEPS, GRADING_CHROMA_RATIO, GRADING_REACH, MAX_DARKEST_L, MAX_GRADING_AMOUNT, MAX_SHIFT_AMOUNT, MIN_LIGHTEST_L, PERCEPTUAL_JND, SHIFT_REACH, apcaContrast, apcaToNormalized, buildOneSidedGrade, clampSrgb, contrastTextHex, deltaEOK, deltaEOKLab, gamutMap, generateScale, gradeHue, gradingInfluence, hexToOklch, hexToSrgb, isInGamut, keyColor, lightnessToScaleT, linearChannelToSrgb, linearP3ToOklab, linearSrgbToOklab, linearSrgbToSrgb, maxChroma, mix, normalizedToApca, oklabToLinearP3, oklabToLinearSrgb, oklabToOklch, oklabToSrgb, oklchToHex, oklchToOklab, oklchToP3, oklchToSrgb, p3ToOklch, resolveGradedHue, srgbChannelToLinear, srgbToHex, srgbToLinearSrgb, srgbToOklab, srgbToOklch, wcagContrast, wcagLuminance };
|
|
552
601
|
//# sourceMappingURL=index.js.map
|
|
553
602
|
//# sourceMappingURL=index.js.map
|