@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/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 GRADE_REACH = 3 / 5;
349
- var MAX_GRADE_INTENSITY = 0.25;
350
- var LOCAL_GRADE_REACH = 2 / 3;
351
- var MAX_LOCAL_GRADE_INTENSITY = 0.5;
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 = GRADE_REACH, maxIntensity = MAX_GRADE_INTENSITY) {
356
- const { lightHue, lightValue, darkHue, darkValue } = grade;
357
- const li = Math.max(0, Math.min(maxIntensity, lightValue));
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 toRad = Math.PI / 180;
363
- const baseRad = baseHue * toRad;
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 = lightHue * toRad;
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 = darkHue * toRad;
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) / toRad + 360) % 360;
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, value, side) {
392
- return side === "light" ? { lightHue: hue, lightValue: value, darkHue: 0, darkValue: 0 } : { lightHue: 0, lightValue: 0, darkHue: hue, darkValue: value };
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
- function generateScale(options) {
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
- hue,
399
- steps,
464
+ contrast,
465
+ hue = DEFAULT_HUE,
400
466
  chroma,
401
- gamut = "srgb",
402
- lightest = 1,
403
- darkest = 0,
467
+ isP3 = false,
404
468
  grading,
405
- gradient
469
+ shift
406
470
  } = options;
407
- const chromaRatio = chroma?.value ?? 1;
408
- const chromaPeak = chroma?.offset ?? 0.5;
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 = gradient ? buildOneSidedGrade(gradient.hue, gradient.value, gradient.direction) : void 0;
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 = Math.max(0, Math.min(1, lightest));
415
- const darkestL = Math.max(0, Math.min(1, darkest));
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
- const h = hueAt(t);
423
- const C = maxChroma(L, h, gamut) * ratio;
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, hueAt(t), gamut);
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
- const h = hueAt(t);
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, hueAt(tWarped), gamut) * ratio;
542
+ const warpedC = maxChroma(Lwarped, hue, gamut) * ratio;
473
543
  const boundaryC = maxChroma(L, h, gamut);
474
- const C = Math.min(warpedC, boundaryC);
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, gamut = "srgb") {
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 { gamut = "srgb", lightest = 1, darkest = 0 } = options ?? {};
542
- const resolved = resolveColor(color, gamut);
543
- const offset = lightnessToScaleT(resolved.oklch.L, lightest, darkest);
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: { value: resolved.chromaRatio, offset },
547
- resolved
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 { GRADE_REACH, LOCAL_GRADE_REACH, MAX_DARKEST_L, MAX_GRADE_INTENSITY, MAX_LOCAL_GRADE_INTENSITY, MIN_LIGHTEST_L, PERCEPTUAL_JND, apcaContrast, buildOneSidedGrade, clampSrgb, contrastTextHex, deltaEOK, deltaEOKLab, findNearest, gamutMap, generateScale, gradeHue, hexToOklch, hexToSrgb, isInGamut, keyColor, lightnessToScaleT, linearChannelToSrgb, linearP3ToOklab, linearSrgbToOklab, linearSrgbToSrgb, maxChroma, mix, oklabToLinearP3, oklabToLinearSrgb, oklabToOklch, oklabToSrgb, oklchToHex, oklchToOklab, oklchToP3, oklchToSrgb, p3ToOklch, resolveColor, resolveDarkest, resolveGradedHue, resolveLightest, srgbChannelToLinear, srgbToHex, srgbToLinearSrgb, srgbToOklab, srgbToOklch, wcagContrast, wcagLuminance };
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