···11+/*
22+ * This code was originally written in Java and has been converted to JavaScript.
33+ *
44+ * Original Java code source: [https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/com/android/internal/graphics/cam]
55+ *
66+ * Copyright (C) 2021 The Android Open Source Project.
77+ * The original code is licensed under the Apache License, Version 2.0.
88+ *
99+ * This JavaScript version is licensed under the MIT License.
1010+ *
1111+ * See the respective licenses for the specific terms, permissions, and limitations.
1212+ */
1313+1414+import Color from '../Utils/Color'
1515+import ColorUtils from '../Utils/ColorUtils'
1616+import MathUtils from '../Utils/MathUtils'
1717+import HctSolver from './HctSolver'
1818+1919+/**
2020+ * A color appearance model, based on CAM16, extended to use L* as the lightness dimension, and coupled to a gamut mapping
2121+ * algorithm. Creates a color system, enables a digital design system.
2222+ */
2323+export class Cam {
2424+ // When the delta between the floor & ceiling of a binary search for chroma is less than this,
2525+ // the binary search terminates.
2626+ static CHROMA_SEARCH_ENDPOINT = 0.4
2727+2828+ // When the delta between the floor & ceiling of a binary search for J, lightness in CAM16,
2929+ // is less than this, the binary search terminates.
3030+ static LIGHTNESS_SEARCH_ENDPOINT = 0.01
3131+3232+ // The maximum difference between the requested L* and the L* returned.
3333+ static DL_MAX = 0.2
3434+3535+ // The maximum color distance, in CAM16-UCS, between a requested color and the color returned.
3636+ static DE_MAX = 1
3737+3838+ /** The maximum difference between the requested L* and the L* returned. */
3939+ DL_MAX = 0.2
4040+ /** When the delta between the floor & ceiling of a binary search for chroma is less than this, the binary search terminates. */
4141+ CHROMA_SEARCH_ENDPOINT = 0.4
4242+ /**
4343+ * When the delta between the floor & ceiling of a binary search for J, lightness in CAM16, is less than this, the binary search
4444+ * terminates.
4545+ */
4646+ LIGHTNESS_SEARCH_ENDPOINT = 0.01
4747+ /**
4848+ * SRGB specification has D65 whitepoint - Stokes, Anderson, Chandrasekar, Motta - A Standard Default Color Space for the
4949+ * Internet: sRGB, 1996
5050+ */
5151+ WHITE_POINT_D65: [number, number, number] = [95.047, 100, 108.883]
5252+5353+ // CAM16 color dimensions, see getters for documentation.
5454+ mHue: number
5555+ mChroma: number
5656+ mJ: number
5757+ mQ: number
5858+ mM: number
5959+ mS: number
6060+6161+ // Coordinates in UCS space. Used to determine color distance, like delta E equations in L*a*b*.
6262+ mJstar: number
6363+ mAstar: number
6464+ mBstar: number
6565+6666+ constructor(
6767+ hue: number,
6868+ chroma: number,
6969+ j: number,
7070+ q: number,
7171+ m: number,
7272+ s: number,
7373+ jstar: number,
7474+ astar: number,
7575+ bstar: number,
7676+ ) {
7777+ this.mHue = hue
7878+ this.mChroma = chroma
7979+ this.mJ = j
8080+ this.mQ = q
8181+ this.mM = m
8282+ this.mS = s
8383+ this.mJstar = jstar
8484+ this.mAstar = astar
8585+ this.mBstar = bstar
8686+ }
8787+8888+ /** Hue in CAM16 */
8989+ getHue() {
9090+ return this.mHue
9191+ }
9292+9393+ /** Chroma in CAM16 */
9494+ getChroma() {
9595+ return this.mChroma
9696+ }
9797+9898+ /** Lightness in CAM16 */
9999+ getJ() {
100100+ return this.mJ
101101+ }
102102+103103+ /** A* coordinate in CAM16-UCS */
104104+ getAstar() {
105105+ return this.mAstar
106106+ }
107107+108108+ /** Lightness coordinate in CAM16-UCS */
109109+ getJstar() {
110110+ return this.mJstar
111111+ }
112112+113113+ /** B* coordinate in CAM16-UCS */
114114+ getBstar() {
115115+ return this.mBstar
116116+ }
117117+118118+ /**
119119+ * Create a CAM from lightness, chroma, and hue coordinates. It is assumed those coordinates were measured in the sRGB standard
120120+ * frame.
121121+ */
122122+ static fromJch(j: number, c: number, h: number) {
123123+ return Cam.fromJchInFrame(j, c, h)
124124+ }
125125+126126+ /** Create a CAM from lightness, chroma, and hue coordinates, and also specify the frame in which the color is being viewed. */
127127+ static fromJchInFrame(j: number, c: number, h: number) {
128128+ const q =
129129+ (4 / Frame.DEFAULT.getC()) *
130130+ Math.sqrt(j / 100) *
131131+ (Frame.DEFAULT.getAw() + 4) *
132132+ Frame.DEFAULT.getFlRoot()
133133+ const m = c * Frame.DEFAULT.getFlRoot()
134134+ const alpha = c / Math.sqrt(j / 100)
135135+ const s =
136136+ 50 *
137137+ Math.sqrt((alpha * Frame.DEFAULT.getC()) / (Frame.DEFAULT.getAw() + 4))
138138+139139+ const hueRadians = (h * Math.PI) / 180
140140+ const jstar = ((1 + 100 * 0.007) * j) / (1 + 0.007 * j)
141141+ const mstar = (1 / 0.0228) * Math.log(1 + 0.0228 * m)
142142+ const astar = mstar * Math.cos(hueRadians)
143143+ const bstar = mstar * Math.sin(hueRadians)
144144+ return new Cam(h, c, j, q, m, s, jstar, astar, bstar)
145145+ }
146146+147147+ /** Returns perceived color as an ARGB integer, as viewed in standard sRGB frame. */
148148+ viewedInSrgb() {
149149+ return this.viewed(Frame.DEFAULT)
150150+ }
151151+152152+ /** Returns color perceived in a frame as an ARGB integer. */
153153+ viewed(frame: Frame) {
154154+ const alpha =
155155+ this.getChroma() === 0 || this.getJ() === 0
156156+ ? 0
157157+ : this.getChroma() / Math.sqrt(this.getJ() / 100)
158158+159159+ const t = Math.pow(
160160+ alpha / Math.pow(1.64 - Math.pow(0.29, frame.getN()), 0.73),
161161+ 1 / 0.9,
162162+ ),
163163+ hRad = (this.getHue() * Math.PI) / 180
164164+165165+ const eHue = 0.25 * (Math.cos(hRad + 2) + 3.8),
166166+ ac =
167167+ frame.getAw() *
168168+ Math.pow(this.getJ() / 100, 1 / frame.getC() / frame.getZ()),
169169+ p1 = eHue * (50000 / 13) * frame.getNc() * frame.getNcb(),
170170+ p2 = ac / frame.getNbb()
171171+172172+ const hSin = Math.sin(hRad),
173173+ hCos = Math.cos(hRad)
174174+175175+ const gamma =
176176+ (23 * (p2 + 0.305) * t) / (23 * p1 + 11 * t * hCos + 108 * t * hSin),
177177+ a = gamma * hCos,
178178+ b = gamma * hSin,
179179+ rA = (460 * p2 + 451 * a + 288 * b) / 1403,
180180+ gA = (460 * p2 - 891 * a - 261 * b) / 1403,
181181+ bA = (460 * p2 - 220 * a - 6300 * b) / 1403
182182+183183+ const rCBase = Math.max(0, (27.13 * Math.abs(rA)) / (400 - Math.abs(rA))),
184184+ rC = Math.sign(rA) * (100 / frame.getFl()) * Math.pow(rCBase, 1 / 0.42),
185185+ gCBase = Math.max(0, (27.13 * Math.abs(gA)) / (400 - Math.abs(gA))),
186186+ gC = Math.sign(gA) * (100 / frame.getFl()) * Math.pow(gCBase, 1 / 0.42),
187187+ bCBase = Math.max(0, (27.13 * Math.abs(bA)) / (400 - Math.abs(bA))),
188188+ bC = Math.sign(bA) * (100 / frame.getFl()) * Math.pow(bCBase, 1 / 0.42),
189189+ rF = rC / frame.getRgbD()[0],
190190+ gF = gC / frame.getRgbD()[1],
191191+ bF = bC / frame.getRgbD()[2]
192192+193193+ const matrix = CamUtils.CAM16RGB_TO_XYZ,
194194+ x = rF * matrix[0][0] + gF * matrix[0][1] + bF * matrix[0][2],
195195+ y = rF * matrix[1][0] + gF * matrix[1][1] + bF * matrix[1][2],
196196+ z = rF * matrix[2][0] + gF * matrix[2][1] + bF * matrix[2][2]
197197+198198+ return ColorUtils.XYZToColor(x, y, z)
199199+ }
200200+201201+ /**
202202+ * Distance in CAM16-UCS space between two colors.
203203+ *
204204+ * Much like L_a_b* was designed to measure distance between colors, the CAM16 standard defined a color space called CAM16-UCS
205205+ * to measure distance between CAM16 colors.
206206+ */
207207+ distance(other: Cam) {
208208+ const dJ = this.getJstar() - other.getJstar(),
209209+ dA = this.getAstar() - other.getAstar(),
210210+ dB = this.getBstar() - other.getBstar(),
211211+ dEPrime = Math.sqrt(dJ * dJ + dA * dA + dB * dB),
212212+ dE = 1.41 * Math.pow(dEPrime, 0.63)
213213+ return dE
214214+ }
215215+216216+ /**
217217+ * Find J, lightness in CAM16 color space, that creates a color with L* = `lstar` in the L_a_b* color space. Returns null if no
218218+ * J could be found that generated a color with L* `lstar`.
219219+ */
220220+ static findCamByJ(hue: number, chroma: number, lstar: number) {
221221+ let low = 0,
222222+ high = 100,
223223+ mid,
224224+ bestdL = 1000,
225225+ bestdE = 1000
226226+227227+ let bestCam: Cam | null = null
228228+ while (Math.abs(low - high) > Cam.LIGHTNESS_SEARCH_ENDPOINT) {
229229+ mid = low + (high - low) / 2
230230+231231+ // Create the intended CAM color
232232+ const camBeforeClip = Cam.fromJch(mid, chroma, hue)
233233+234234+ // Convert the CAM color to RGB. If the color didn't fit in RGB, during the conversion,
235235+ // the initial RGB values will be outside 0 to 255. The final RGB values are clipped to
236236+ // 0 to 255, distorting the intended color.
237237+ const clipped = camBeforeClip.viewedInSrgb()
238238+ const clippedLstar = CamUtils.lstarFromInt(clipped)
239239+ const dL = Math.abs(lstar - clippedLstar)
240240+241241+ // If the clipped color's L* is within error margin...
242242+ if (dL < Cam.DL_MAX) {
243243+ // ...check if the CAM equivalent of the clipped color is far away from intended CAM
244244+ // color. For the intended color, use lightness and chroma from the clipped color,
245245+ // and the intended hue. Callers are wondering what the lightness is, they know
246246+ // chroma may be distorted, so the only concern here is if the hue slipped too far.
247247+ const camClipped = Cam.fromInt(clipped)
248248+ const dE = camClipped.distance(
249249+ Cam.fromJch(camClipped.getJ(), camClipped.getChroma(), hue),
250250+ )
251251+ if (dE <= Cam.DE_MAX) {
252252+ bestdL = dL
253253+ bestdE = dE
254254+ bestCam = camClipped
255255+ }
256256+ }
257257+258258+ // If there's no error at all, there's no need to search more.
259259+ //
260260+ // Note: this happens much more frequently than expected, but this is a very delicate
261261+ // property which relies on extremely precise sRGB <=> XYZ calculations, as well as fine
262262+ // tuning of the constants that determine error margins and when the binary search can
263263+ // terminate.
264264+ if (bestdL === 0 && bestdE === 0) {
265265+ break
266266+ }
267267+268268+ if (clippedLstar < lstar) {
269269+ low = mid
270270+ } else {
271271+ high = mid
272272+ }
273273+ }
274274+275275+ return bestCam
276276+ }
277277+278278+ /**
279279+ * Given a hue & chroma in CAM16, L* in L_a_b*, return an ARGB integer. The chroma of the color returned may, and frequently
280280+ * will, be lower than requested. Assumes the color is viewed in the frame defined by the sRGB standard.
281281+ */
282282+ static getInt(hue: number, chroma: number, lstar: number) {
283283+ return Cam.getInt_(hue, chroma, lstar, Frame.DEFAULT)
284284+ }
285285+286286+ /**
287287+ * Given a hue & chroma in CAM16, L* in L_a_b*, and the frame in which the color will be viewed, return an ARGB integer.
288288+ *
289289+ * The chroma of the color returned may, and frequently will, be lower than requested. This is a fundamental property of color
290290+ * that cannot be worked around by engineering. For example, a red hue, with high chroma, and high L* does not exist: red hues
291291+ * have a maximum chroma below 10 in light shades, creating pink.
292292+ */
293293+ static getInt_(hue: number, chroma: number, lstar: number, frame: Frame) {
294294+ // This is a crucial routine for building a color system, CAM16 itself is not sufficient.
295295+ //
296296+ // * Why these dimensions?
297297+ // Hue and chroma from CAM16 are used because they're the most accurate measures of those
298298+ // quantities. L* from L*a*b* is used because it correlates with luminance, luminance is
299299+ // used to measure contrast for a11y purposes, thus providing a key constraint on what
300300+ // colors
301301+ // can be used.
302302+ //
303303+ // * Why is this routine required to build a color system?
304304+ // In all perceptually accurate color spaces (i.e. L*a*b* and later), `chroma` may be
305305+ // impossible for a given `hue` and `lstar`.
306306+ // For example, a high chroma light red does not exist - chroma is limited to below 10 at
307307+ // light red shades, we call that pink. High chroma light green does exist, but not dark
308308+ // Also, when converting from another color space to RGB, the color may not be able to be
309309+ // represented in RGB. In those cases, the conversion process ends with RGB values
310310+ // outside 0-255
311311+ // The vast majority of color libraries surveyed simply round to 0 to 255. That is not an
312312+ // option for this library, as it distorts the expected luminance, and thus the expected
313313+ // contrast needed for a11y
314314+ //
315315+ // * What does this routine do?
316316+ // Dealing with colors in one color space not fitting inside RGB is, loosely referred to as
317317+ // gamut mapping or tone mapping. These algorithms are traditionally idiosyncratic, there is
318318+ // no universal answer. However, because the intent of this library is to build a system for
319319+ // digital design, and digital design uses luminance to measure contrast/a11y, we have one
320320+ // very important constraint that leads to an objective algorithm: the L* of the returned
321321+ // color _must_ match the requested L*.
322322+ //
323323+ // Intuitively, if the color must be distorted to fit into the RGB gamut, and the L*
324324+ // requested *must* be fulfilled, than the hue or chroma of the returned color will need
325325+ // to be different from the requested hue/chroma.
326326+ //
327327+ // After exploring both options, it was more intuitive that if the requested chroma could
328328+ // not be reached, it used the highest possible chroma. The alternative was finding the
329329+ // closest hue where the requested chroma could be reached, but that is not nearly as
330330+ // intuitive, as the requested hue is so fundamental to the color description.
331331+332332+ // If the color doesn't have meaningful chroma, return a gray with the requested Lstar.
333333+ //
334334+ // Yellows are very chromatic at L = 100, and blues are very chromatic at L = 0. All the
335335+ // other hues are white at L = 100, and black at L = 0. To preserve consistency for users of
336336+ // this system, it is better to simply return white at L* > 99, and black and L* < 0.
337337+ if (frame === Frame.DEFAULT) {
338338+ // If the viewing conditions are the same as the default sRGB-like viewing conditions,
339339+ // skip to using HctSolver: it uses geometrical insights to find the closest in-gamut
340340+ // match to hue/chroma/lstar.
341341+ return HctSolver.solveToInt(hue, chroma, lstar)
342342+ }
343343+344344+ if (chroma < 1 || Math.round(lstar) <= 0 || Math.round(lstar) >= 100) {
345345+ return CamUtils.intFromLstar(lstar)
346346+ }
347347+348348+ hue = hue < 0 ? 0 : Math.min(360, hue)
349349+350350+ // The highest chroma possible. Updated as binary search proceeds.
351351+ let high = chroma
352352+353353+ // The guess for the current binary search iteration. Starts off at the highest chroma,
354354+ // thus, if a color is possible at the requested chroma, the search can stop after one try.
355355+ let mid = chroma
356356+ let low = 0
357357+ let isFirstLoop = true
358358+359359+ let answer: Cam | null = null
360360+361361+ while (Math.abs(low - high) >= Cam.CHROMA_SEARCH_ENDPOINT) {
362362+ // Given the current chroma guess, mid, and the desired hue, find J, lightness in
363363+ // CAM16 color space, that creates a color with L* = `lstar` in the L*a*b* color space.
364364+ const possibleAnswer = Cam.findCamByJ(hue, mid, lstar)
365365+366366+ if (isFirstLoop) {
367367+ if (possibleAnswer != null) {
368368+ return possibleAnswer.viewed(frame)
369369+ } else {
370370+ // If this binary search iteration was the first iteration, and this point
371371+ // has been reached, it means the requested chroma was not available at the
372372+ // requested hue and L*.
373373+ // Proceed to a traditional binary search that starts at the midpoint between
374374+ // the requested chroma and 0.
375375+ isFirstLoop = false
376376+ mid = low + (high - low) / 2
377377+ continue
378378+ }
379379+ }
380380+381381+ if (possibleAnswer == null) {
382382+ // There isn't a CAM16 J that creates a color with L* `lstar`. Try a lower chroma.
383383+ high = mid
384384+ } else {
385385+ answer = possibleAnswer
386386+ // It is possible to create a color. Try higher chroma.
387387+ low = mid
388388+ }
389389+390390+ mid = low + (high - low) / 2
391391+ }
392392+393393+ // There was no answer: meaning, for the desired hue, there was no chroma low enough to
394394+ // generate a color with the desired L*.
395395+ // All values of L* are possible when there is 0 chroma. Return a color with 0 chroma, i.e.
396396+ // a shade of gray, with the desired L*.
397397+ if (answer == null) {
398398+ return CamUtils.intFromLstar(lstar)
399399+ }
400400+401401+ return answer.viewed(frame)
402402+ }
403403+404404+ static intFromLstar(lstar: number) {
405405+ if (lstar < 1) {
406406+ return 0xff000000
407407+ } else if (lstar > 99) {
408408+ return 0xffffffff
409409+ }
410410+411411+ // XYZ to LAB conversion routine, assume a and b are 0.
412412+ const fy = (lstar + 16) / 116
413413+414414+ // fz = fx = fy because a and b are 0
415415+ const fz = fy,
416416+ fx = fy
417417+418418+ const kappa = 24389 / 27,
419419+ epsilon = 216 / 24389,
420420+ lExceedsEpsilonKappa = lstar > 8,
421421+ yT = lExceedsEpsilonKappa ? fy * fy * fy : lstar / kappa,
422422+ cubeExceedEpsilon = fy * fy * fy > epsilon,
423423+ xT = cubeExceedEpsilon ? fx * fx * fx : (116 * fx - 16) / kappa,
424424+ zT = cubeExceedEpsilon ? fz * fz * fz : (116 * fx - 16) / kappa
425425+426426+ return ColorUtils.XYZToColor(
427427+ xT * CamUtils.WHITE_POINT_D65[0],
428428+ yT * CamUtils.WHITE_POINT_D65[1],
429429+ zT * CamUtils.WHITE_POINT_D65[2],
430430+ )
431431+ }
432432+433433+ static fromIntInFrame(argb: number, frame: Frame) {
434434+ // Transform ARGB int to XYZ
435435+ const xyz: [number, number, number] = CamUtils.xyzFromInt(argb)
436436+437437+ // Transform XYZ to 'cone'/'rgb' responses
438438+ const matrix = CamUtils.XYZ_TO_CAM16RGB,
439439+ rT: number =
440440+ xyz[0] * matrix[0][0] + xyz[1] * matrix[0][1] + xyz[2] * matrix[0][2],
441441+ gT: number =
442442+ xyz[0] * matrix[1][0] + xyz[1] * matrix[1][1] + xyz[2] * matrix[1][2],
443443+ bT: number =
444444+ xyz[0] * matrix[2][0] + xyz[1] * matrix[2][1] + xyz[2] * matrix[2][2]
445445+446446+ // Discount illuminant
447447+ const rD = frame.getRgbD()[0] * rT,
448448+ gD = frame.getRgbD()[1] * gT,
449449+ bD = frame.getRgbD()[2] * bT
450450+451451+ // Chromatic adaptation
452452+ const rAF = Math.pow((frame.getFl() * Math.abs(rD)) / 100, 0.42),
453453+ gAF = Math.pow((frame.getFl() * Math.abs(gD)) / 100, 0.42),
454454+ bAF = Math.pow((frame.getFl() * Math.abs(bD)) / 100, 0.42),
455455+ rA = (Math.sign(rD) * 400 * rAF) / (rAF + 27.13),
456456+ gA = (Math.sign(gD) * 400 * gAF) / (gAF + 27.13),
457457+ bA = (Math.sign(bD) * 400 * bAF) / (bAF + 27.13)
458458+459459+ // redness-greennes
460460+ const a = (11 * rA + -12 * gA + bA) / 11
461461+ // yellowness-blueness
462462+ const b = (rA + gA - 2 * bA) / 9
463463+464464+ // auxiliary components
465465+ const u = (20 * rA + 20 * gA + 21 * bA) / 20,
466466+ p2 = (40 * rA + 20 * gA + bA) / 20
467467+468468+ // hue
469469+ const atan2 = Math.atan2(b, a),
470470+ atanDegrees = (atan2 * 180) / Math.PI,
471471+ hue =
472472+ atanDegrees < 0
473473+ ? atanDegrees + 360
474474+ : atanDegrees >= 360
475475+ ? atanDegrees - 360
476476+ : atanDegrees,
477477+ hueRadians = (hue * Math.PI) / 180
478478+479479+ // achromatic response to color
480480+ const ac = p2 * frame.getNbb()
481481+482482+ // CAM16 lightness and brightness
483483+ const j = 100 * Math.pow(ac / frame.getAw(), frame.getC() * frame.getZ())
484484+ const q =
485485+ (4 / frame.getC()) *
486486+ Math.sqrt(j / 100) *
487487+ (frame.getAw() + 4) *
488488+ frame.getFlRoot()
489489+490490+ // CAM16 chroma, colorfulness, and saturation.
491491+ const huePrime = hue < 20.14 ? hue + 360 : hue,
492492+ eHue = 0.25 * (Math.cos((huePrime * Math.PI) / 180 + 2) + 3.8),
493493+ p1 = (50000 / 13) * eHue * frame.getNc() * frame.getNcb(),
494494+ t = (p1 * Math.sqrt(a * a + b * b)) / (u + 0.305),
495495+ alpha =
496496+ Math.pow(t, 0.9) * Math.pow(1.64 - Math.pow(0.29, frame.getN()), 0.73)
497497+498498+ // CAM16 chroma, colorfulness, saturation
499499+ const c = alpha * Math.sqrt(j / 100),
500500+ m = c * frame.getFlRoot(),
501501+ s = 50 * Math.sqrt((alpha * frame.getC()) / (frame.getAw() + 4))
502502+503503+ // CAM16-UCS components
504504+ const jstar = ((1 + 100 * 0.007) * j) / (1 + 0.007 * j),
505505+ mstar = (1 / 0.0228) * Math.log(1 + 0.0228 * m),
506506+ astar = mstar * Math.cos(hueRadians),
507507+ bstar = mstar * Math.sin(hueRadians)
508508+509509+ return new Cam(hue, c, j, q, m, s, jstar, astar, bstar)
510510+ }
511511+512512+ /**
513513+ * Create a color appearance model from a ARGB integer representing a color. It is assumed the color was viewed in the frame
514514+ * defined in the sRGB standard.
515515+ */
516516+ static fromInt(argb: number) {
517517+ return Cam.fromIntInFrame(argb, Frame.DEFAULT)
518518+ }
519519+}
520520+521521+/**
522522+ * Collection of methods for transforming between color spaces.
523523+ *
524524+ * Methods are named $xFrom$Y. For example, lstarFromInt() returns L* from an ARGB integer.
525525+ *
526526+ * These methods, generally, convert colors between the L_a_b*, XYZ, and sRGB spaces.
527527+ *
528528+ * L_a_b* is a perceptually accurate color space. This is particularly important in the L* dimension: it measures luminance and
529529+ * unlike lightness measures traditionally used in UI work via RGB or HSL, this luminance transitions smoothly, permitting
530530+ * creation of pleasing shades of a color, and more pleasing transitions between colors.
531531+ *
532532+ * XYZ is commonly used as an intermediate color space for converting between one color space to another. For example, to convert
533533+ * RGB to L_a_b*, first RGB is converted to XYZ, then XYZ is converted to L_a_b*.
534534+ *
535535+ * SRGB is a "specification originated from work in 1990s through cooperation by Hewlett-Packard and Microsoft, and it was
536536+ * designed to be a standard definition of RGB for the internet, which it indeed became...The standard is based on a sampling of
537537+ * computer monitors at the time...The whole idea of sRGB is that if everyone assumed that RGB meant the same thing, then the
538538+ * results would be consistent, and reasonably good. It worked." - Fairchild, Color Models and Systems: Handbook of Color
539539+ * Psychology, 2015
540540+ */
541541+export class CamUtils {
542542+ /**
543543+ * This is a more precise sRGB to XYZ transformation matrix than traditionally used. It was derived using Schlomer's technique
544544+ * of transforming the xyY primaries to XYZ, then applying a correction to ensure mapping from sRGB 1, 1, 1 to the reference
545545+ * white point, D65.
546546+ */
547547+ static SRGB_TO_XYZ: [
548548+ [number, number, number],
549549+ [number, number, number],
550550+ [number, number, number],
551551+ ] = [
552552+ [0.41233895, 0.35762064, 0.18051042],
553553+ [0.2126, 0.7152, 0.0722],
554554+ [0.01932141, 0.11916382, 0.95034478],
555555+ ]
556556+557557+ /** Transforms XYZ color space coordinates to 'cone'/'RGB' responses in CAM16. */
558558+ static XYZ_TO_CAM16RGB: [
559559+ [number, number, number],
560560+ [number, number, number],
561561+ [number, number, number],
562562+ ] = [
563563+ [0.401288, 0.650173, -0.051461],
564564+ [-0.250268, 1.204414, 0.045854],
565565+ [-0.002079, 0.048952, 0.953127],
566566+ ]
567567+568568+ static XYZ_TO_SRGB: [
569569+ [number, number, number],
570570+ [number, number, number],
571571+ [number, number, number],
572572+ ] = [
573573+ [3.2413774792388685, -1.5376652402851851, -0.49885366846268053],
574574+ [-0.9691452513005321, 1.8758853451067872, 0.04156585616912061],
575575+ [0.05562093689691305, -0.20395524564742123, 1.0571799111220335],
576576+ ]
577577+578578+ /** Transforms 'cone'/'RGB' responses in CAM16 to XYZ color space coordinates. */
579579+ static CAM16RGB_TO_XYZ: [
580580+ [number, number, number],
581581+ [number, number, number],
582582+ [number, number, number],
583583+ ] = [
584584+ [1.86206786, -1.01125463, 0.14918677],
585585+ [0.38752654, 0.62144744, -0.00897398],
586586+ [-0.0158415, -0.03412294, 1.04996444],
587587+ ]
588588+589589+ /**
590590+ * SRGB specification has D65 whitepoint - Stokes, Anderson, Chandrasekar, Motta - A Standard Default Color Space for the
591591+ * Internet: sRGB, 1996
592592+ */
593593+ static WHITE_POINT_D65: [number, number, number] = [95.047, 100, 108.883]
594594+595595+ /** Returns L* from L_a_b*, perceptual luminance, from an ARGB integer (ColorInt). */
596596+ static lstarFromInt(argb: number) {
597597+ return CamUtils.lstarFromY(CamUtils.yFromInt(argb))
598598+ }
599599+600600+ static lstarFromY(y: number) {
601601+ y = y / 100
602602+ const e = 216 / 24389
603603+ let yIntermediate
604604+ if (y <= e) {
605605+ return (24389 / 27) * y
606606+ } else {
607607+ yIntermediate = Math.cbrt(y)
608608+ }
609609+ return 116 * yIntermediate - 16
610610+ }
611611+612612+ static yFromInt(argb: number) {
613613+ const r = CamUtils.linearized(Color.red(argb)),
614614+ g = CamUtils.linearized(Color.green(argb)),
615615+ b = CamUtils.linearized(Color.blue(argb)),
616616+ matrix = CamUtils.SRGB_TO_XYZ,
617617+ y = r * matrix[1][0] + g * matrix[1][1] + b * matrix[1][2]
618618+ return y
619619+ }
620620+621621+ static xyzFromInt(argb: number): [number, number, number] {
622622+ const r = CamUtils.linearized(Color.red(argb)),
623623+ g = CamUtils.linearized(Color.green(argb)),
624624+ b = CamUtils.linearized(Color.blue(argb))
625625+626626+ const matrix = CamUtils.SRGB_TO_XYZ,
627627+ x = r * matrix[0][0] + g * matrix[0][1] + b * matrix[0][2],
628628+ y = r * matrix[1][0] + g * matrix[1][1] + b * matrix[1][2],
629629+ z = r * matrix[2][0] + g * matrix[2][1] + b * matrix[2][2]
630630+631631+ return [x, y, z]
632632+ }
633633+634634+ static linearized(rgbComponent: number): number {
635635+ const normalized = rgbComponent / 255
636636+637637+ if (normalized <= 0.04045) {
638638+ return (normalized / 12.92) * 100
639639+ } else {
640640+ return Math.pow((normalized + 0.055) / 1.055, 2.4) * 100
641641+ }
642642+ }
643643+644644+ /**
645645+ * Converts an L* value to a Y value.
646646+ *
647647+ * L* in L_a_b* and Y in XYZ measure the same quantity, luminance.
648648+ *
649649+ * L* measures perceptual luminance, a linear scale. Y in XYZ measures relative luminance, a logarithmic scale.
650650+ *
651651+ * @param lstar L* in L_a_b*
652652+ * @returns Y in XYZ
653653+ */
654654+ static yFromLstar(lstar: number) {
655655+ const ke = 8
656656+ if (lstar > ke) {
657657+ return Math.pow((lstar + 16) / 116, 3) * 100
658658+ } else {
659659+ return (lstar / (24389 / 27)) * 100
660660+ }
661661+ }
662662+663663+ /**
664664+ * Clamps an integer between two integers.
665665+ *
666666+ * @returns Input when min <= input <= max, and either min or max otherwise.
667667+ */
668668+ static clampInt(min: number, max: number, input: number) {
669669+ if (input < min) {
670670+ return min
671671+ } else if (input > max) {
672672+ return max
673673+ }
674674+675675+ return input
676676+ }
677677+678678+ /**
679679+ * Delinearizes an RGB component.
680680+ *
681681+ * @param rgbComponent 0 <= rgb_component <= 100, represents linear R/G/B channel
682682+ * @returns 0 <= output <= 255, color channel converted to regular RGB space
683683+ */
684684+ static delinearized(rgbComponent: number) {
685685+ const normalized = rgbComponent / 100
686686+ let delinearized = 0
687687+ if (normalized <= 0.0031308) {
688688+ delinearized = normalized * 12.92
689689+ } else {
690690+ delinearized = 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055
691691+ }
692692+ return CamUtils.clampInt(0, 255, Math.round(delinearized * 255))
693693+ }
694694+695695+ /** Converts a color from RGB components to ARGB format. */
696696+ static argbFromRgb(red: number, green: number, blue: number) {
697697+ return (
698698+ (255 << 24) | ((red & 255) << 16) | ((green & 255) << 8) | (blue & 255)
699699+ )
700700+ }
701701+702702+ /** Converts a color from ARGB to XYZ. */
703703+ static argbFromXyz(x: number, y: number, z: number) {
704704+ const matrix = CamUtils.XYZ_TO_SRGB,
705705+ linearR = matrix[0][0] * x + matrix[0][1] * y + matrix[0][2] * z,
706706+ linearG = matrix[1][0] * x + matrix[1][1] * y + matrix[1][2] * z,
707707+ linearB = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2] * z,
708708+ r = CamUtils.delinearized(linearR),
709709+ g = CamUtils.delinearized(linearG),
710710+ b = CamUtils.delinearized(linearB)
711711+712712+ return CamUtils.argbFromRgb(r, g, b)
713713+ }
714714+715715+ /**
716716+ * Convert a color appearance model representation to an ARGB color.
717717+ *
718718+ * Note: the returned color may have a lower chroma than requested. Whether a chroma is available depends on luminance. For
719719+ * example, there's no such thing as a high chroma light red, due to the limitations of our eyes and/or physics. If the
720720+ * requested chroma is unavailable, the highest possible chroma at the requested luminance is returned.
721721+ *
722722+ * @param hue Hue, in degrees, in CAM coordinates
723723+ * @param chroma Chroma in CAM coordinates.
724724+ * @param lstar Perceptual luminance, L* in L_a_b*
725725+ */
726726+ static CAMToColor(hue: number, chroma: number, lstar: number) {
727727+ return Cam.getInt(hue, chroma, lstar)
728728+ }
729729+730730+ /**
731731+ * Converts an L* value to an ARGB representation.
732732+ *
733733+ * @param lstar L* in L_a_b*
734734+ * @returns ARGB representation of grayscale color with lightness matching L*
735735+ */
736736+ static argbFromLstar(lstar: number) {
737737+ const fy = (lstar + 16) / 116,
738738+ fz = fy,
739739+ fx = fy,
740740+ kappa = 24389 / 27,
741741+ epsilon = 216 / 24389,
742742+ lExceedsEpsilonKappa = lstar > 8,
743743+ y = lExceedsEpsilonKappa ? fy * fy * fy : lstar / kappa,
744744+ cubeExceedEpsilon = fy * fy * fy > epsilon,
745745+ x = cubeExceedEpsilon ? fx * fx * fx : lstar / kappa,
746746+ z = cubeExceedEpsilon ? fz * fz * fz : lstar / kappa,
747747+ whitePoint = CamUtils.WHITE_POINT_D65
748748+749749+ return CamUtils.argbFromXyz(
750750+ x * whitePoint[0],
751751+ y * whitePoint[1],
752752+ z * whitePoint[2],
753753+ )
754754+ }
755755+756756+ /** Converts a color from linear RGB components to ARGB format. */
757757+ static argbFromLinrgbComponents(r: number, g: number, b: number) {
758758+ return CamUtils.argbFromRgb(
759759+ CamUtils.delinearized(r),
760760+ CamUtils.delinearized(g),
761761+ CamUtils.delinearized(b),
762762+ )
763763+ }
764764+765765+ /**
766766+ * The signum function.
767767+ *
768768+ * @returns 1 if num > 0, -1 if num < 0, and 0 if num = 0
769769+ */
770770+ static signum(num: number) {
771771+ if (num < 0) {
772772+ return -1
773773+ } else if (num === 0) {
774774+ return 0
775775+ } else {
776776+ return 1
777777+ }
778778+ }
779779+780780+ static intFromLstar(lstar: number) {
781781+ if (lstar < 1) {
782782+ return 0xff000000
783783+ } else if (lstar > 99) {
784784+ return 0xffffffff
785785+ }
786786+787787+ // XYZ to LAB conversion routine, assume a and b are 0.
788788+ const fy = (lstar + 16) / 116
789789+790790+ // fz = fx = fy because a and b are 0
791791+ const fz = fy
792792+ const fx = fy
793793+794794+ const kappa = 24389 / 27
795795+ const epsilon = 216 / 24389
796796+ const lExceedsEpsilonKappa = lstar > 8
797797+ const yT = lExceedsEpsilonKappa ? fy * fy * fy : lstar / kappa
798798+ const cubeExceedEpsilon = fy * fy * fy > epsilon
799799+ const xT = cubeExceedEpsilon ? fx * fx * fx : (116 * fx - 16) / kappa
800800+ const zT = cubeExceedEpsilon ? fz * fz * fz : (116 * fx - 16) / kappa
801801+802802+ return ColorUtils.XYZToColor(
803803+ xT * CamUtils.WHITE_POINT_D65[0],
804804+ yT * CamUtils.WHITE_POINT_D65[1],
805805+ zT * CamUtils.WHITE_POINT_D65[2],
806806+ )
807807+ }
808808+}
809809+810810+/**
811811+ * The frame, or viewing conditions, where a color was seen. Used, along with a color, to create a color appearance model
812812+ * representing the color.
813813+ *
814814+ * To convert a traditional color to a color appearance model, it requires knowing what conditions the color was observed in. Our
815815+ * perception of color depends on, for example, the tone of the light illuminating the color, how bright that light was, etc.
816816+ *
817817+ * This class is modelled separately from the color appearance model itself because there are a number of calculations during the
818818+ * color => CAM conversion process that depend only on the viewing conditions. Caching those calculations in a Frame instance
819819+ * saves a significant amount of time.
820820+ */
821821+export class Frame {
822822+ mN: number
823823+ mAw: number
824824+ mNbb: number
825825+ mNcb: number
826826+ mC: number
827827+ mNc: number
828828+ mRgbD: [number, number, number]
829829+ mFl: number
830830+ mFlRoot: number
831831+ mZ: number
832832+833833+ constructor(
834834+ n: number,
835835+ aw: number,
836836+ nbb: number,
837837+ ncb: number,
838838+ c: number,
839839+ nc: number,
840840+ rgbD: [number, number, number],
841841+ fl: number,
842842+ fLRoot: number,
843843+ z: number,
844844+ ) {
845845+ this.mN = n
846846+ this.mAw = aw
847847+ this.mNbb = nbb
848848+ this.mNcb = ncb
849849+ this.mC = c
850850+ this.mNc = nc
851851+ this.mRgbD = rgbD
852852+ this.mFl = fl
853853+ this.mFlRoot = fLRoot
854854+ this.mZ = z
855855+ }
856856+857857+ getRgbD() {
858858+ return this.mRgbD
859859+ }
860860+ getFl() {
861861+ return this.mFl
862862+ }
863863+ getNbb() {
864864+ return this.mNbb
865865+ }
866866+ getAw() {
867867+ return this.mAw
868868+ }
869869+ getC() {
870870+ return this.mC
871871+ }
872872+ getFlRoot() {
873873+ return this.mFlRoot
874874+ }
875875+ getNc() {
876876+ return this.mNc
877877+ }
878878+ getNcb() {
879879+ return this.mNcb
880880+ }
881881+ getZ() {
882882+ return this.mZ
883883+ }
884884+ getN() {
885885+ return this.mN
886886+ }
887887+888888+ static make(
889889+ whitepoint: [number, number, number],
890890+ adaptingLuminance: number,
891891+ backgroundLstar: number,
892892+ surround: number,
893893+ discountingIlluminant: boolean,
894894+ ) {
895895+ // Transform white point XYZ to 'cone'/'rgb' responses
896896+ const matrix = CamUtils.XYZ_TO_CAM16RGB,
897897+ xyz = whitepoint,
898898+ rW =
899899+ xyz[0] * matrix[0][0] + xyz[1] * matrix[0][1] + xyz[2] * matrix[0][2],
900900+ gW =
901901+ xyz[0] * matrix[1][0] + xyz[1] * matrix[1][1] + xyz[2] * matrix[1][2],
902902+ bW = xyz[0] * matrix[2][0] + xyz[1] * matrix[2][1] + xyz[2] * matrix[2][2]
903903+904904+ // Scale input surround, domain (0, 2), to CAM16 surround, domain (0.8, 1)
905905+ const f = 0.8 + surround / 10
906906+ // "Exponential non-linearity"
907907+ const c =
908908+ f >= 0.9
909909+ ? MathUtils.lerp(0.59, 0.69, (f - 0.9) * 10)
910910+ : MathUtils.lerp(0.525, 0.59, (f - 0.8) * 10)
911911+ // Calculate degree of adaptation to illuminant
912912+ let d = discountingIlluminant
913913+ ? 1
914914+ : f * (1 - (1 / 3.6) * Math.exp((-adaptingLuminance - 42) / 92))
915915+ // Per Li et al, if D is greater than 1 or less than 0, set it to 1 or 0.
916916+ d = d > 1 ? 1 : d < 0 ? 0 : d
917917+ // Chromatic induction factor
918918+ const nc = f
919919+920920+ // Cone responses to the whitepoint, adjusted for illuminant discounting.
921921+ //
922922+ // Why use 100 instead of the white point's relative luminance?
923923+ //
924924+ // Some papers and implementations, for both CAM02 and CAM16, use the Y
925925+ // value of the reference white instead of 100. Fairchild's Color Appearance
926926+ // Models (3rd edition) notes that this is in error: it was included in the
927927+ // CIE 2004a report on CIECAM02, but, later parts of the conversion process
928928+ // account for scaling of appearance relative to the white point relative
929929+ // luminance. This part should simply use 100 as luminance.
930930+ const rgbD = [
931931+ d * (100 / rW) + 1 - d,
932932+ d * (100 / gW) + 1 - d,
933933+ d * (100 / bW) + 1 - d,
934934+ ] as [number, number, number]
935935+ // Luminance-level adaptation factor
936936+ const k = 1 / (5 * adaptingLuminance + 1)
937937+ const k4 = k * k * k * k
938938+ const k4F = 1 - k4
939939+ const fl =
940940+ k4 * adaptingLuminance +
941941+ 0.1 * k4F * k4F * Math.cbrt(5 * adaptingLuminance)
942942+943943+ // Intermediate factor, ratio of background relative luminance to white relative luminance
944944+ const n = CamUtils.yFromLstar(backgroundLstar) / whitepoint[1]
945945+946946+ // Base exponential nonlinearity
947947+ // note Schlomer 2018 has a typo and uses 1.58, the correct factor is 1.48
948948+ const z = 1.48 + Math.sqrt(n)
949949+950950+ // Luminance-level induction factors
951951+ const nbb = 0.725 / Math.pow(n, 0.2)
952952+ const ncb = nbb
953953+954954+ // Discounted cone responses to the white point, adjusted for post-chromatic
955955+ // adaptation perceptual nonlinearities.
956956+ const rgbAFactors = [
957957+ Math.pow((fl * rgbD[0] * rW) / 100, 0.42),
958958+ Math.pow((fl * rgbD[1] * gW) / 100, 0.42),
959959+ Math.pow((fl * rgbD[2] * bW) / 100, 0.42),
960960+ ] as const
961961+962962+ const rgbA = [
963963+ (400 * rgbAFactors[0]) / (rgbAFactors[0] + 27.13),
964964+ (400 * rgbAFactors[1]) / (rgbAFactors[1] + 27.13),
965965+ (400 * rgbAFactors[2]) / (rgbAFactors[2] + 27.13),
966966+ ] as const
967967+968968+ const aw = (2 * rgbA[0] + rgbA[1] + 0.05 * rgbA[2]) * nbb
969969+970970+ return new Frame(n, aw, nbb, ncb, c, nc, rgbD, fl, Math.pow(fl, 0.25), z)
971971+ }
972972+973973+ static DEFAULT = Frame.make(
974974+ CamUtils.WHITE_POINT_D65,
975975+ ((200 / Math.PI) * CamUtils.yFromLstar(50)) / 100,
976976+ 50,
977977+ 2,
978978+ false,
979979+ )
980980+}
+574
src/alf/util/material3/Cam/HctSolver.ts
···11+/*
22+ * This code was originally written in Java and has been converted to JavaScript.
33+ *
44+ * Original Java code source: [https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/com/android/internal/graphics/cam]
55+ *
66+ * Copyright (C) 2021 The Android Open Source Project.
77+ * The original code is licensed under the Apache License, Version 2.0.
88+ *
99+ * This JavaScript version is licensed under the MIT License.
1010+ *
1111+ * See the respective licenses for the specific terms, permissions, and limitations.
1212+ */
1313+1414+import MathUtils from '../Utils/MathUtils'
1515+import {CamUtils, Frame} from './Cam'
1616+1717+export default class HctSolver {
1818+ /** Weights for transforming a set of linear RGB coordinates to Y in XYZ. */
1919+ static Y_FROM_LINRGB: [number, number, number] = [0.2126, 0.7152, 0.0722]
2020+2121+ // Matrix used when converting from CAM16 to linear RGB.
2222+ static LINRGB_FROM_SCALED_DISCOUNT: [
2323+ [number, number, number],
2424+ [number, number, number],
2525+ [number, number, number],
2626+ ] = [
2727+ [1373.2198709594231, -1100.4251190754821, -7.278681089101213],
2828+ [-271.815969077903, 559.6580465940733, -32.46047482791194],
2929+ [1.9622899599665666, -57.173814538844006, 308.7233197812385],
3030+ ]
3131+3232+ /** Matrix used when converting from linear RGB to CAM16. */
3333+ static SCALED_DISCOUNT_FROM_LINRGB: [
3434+ [number, number, number],
3535+ [number, number, number],
3636+ [number, number, number],
3737+ ] = [
3838+ [0.001200833568784504, 0.002389694492170889, 0.0002795742885861124],
3939+ [0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398],
4040+ [0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076],
4141+ ]
4242+4343+ /**
4444+ * Lookup table for plane in XYZ's Y axis (relative luminance) that corresponds to a given L* in L_a_b*. HCT's T is L*, and
4545+ * XYZ's Y is directly correlated to linear RGB, this table allows us to thus find the intersection between HCT and RGB, giving
4646+ * a solution to the RGB coordinates that correspond to a given set of HCT coordinates.
4747+ */
4848+ static CRITICAL_PLANES = [
4949+ 0.015176349177441876, 0.045529047532325624, 0.07588174588720938,
5050+ 0.10623444424209313, 0.13658714259697685, 0.16693984095186062,
5151+ 0.19729253930674434, 0.2276452376616281, 0.2579979360165119,
5252+ 0.28835063437139563, 0.3188300904430532, 0.350925934958123,
5353+ 0.3848314933096426, 0.42057480301049466, 0.458183274052838,
5454+ 0.4976837250274023, 0.5391024159806381, 0.5824650784040898,
5555+ 0.6277969426914107, 0.6751227633498623, 0.7244668422128921,
5656+ 0.775853049866786, 0.829304845476233, 0.8848452951698498, 0.942497089126609,
5757+ 1.0022825574869039, 1.0642236851973577, 1.1283421258858297,
5858+ 1.1946592148522128, 1.2631959812511864, 1.3339731595349034,
5959+ 1.407011200216447, 1.4823302800086415, 1.5599503113873272,
6060+ 1.6398909516233677, 1.7221716113234105, 1.8068114625156377,
6161+ 1.8938294463134073, 1.9832442801866852, 2.075074464868551,
6262+ 2.1693382909216234, 2.2660538449872063, 2.36523901573795,
6363+ 2.4669114995532007, 2.5710888059345764, 2.6777882626779785,
6464+ 2.7870270208169257, 2.898822059350997, 3.0131901897720907,
6565+ 3.1301480604002863, 3.2497121605402226, 3.3718988244681087,
6666+ 3.4967242352587946, 3.624204428461639, 3.754355295633311, 3.887192587735158,
6767+ 4.022731918402185, 4.160988767090289, 4.301978482107941, 4.445716283538092,
6868+ 4.592217266055746, 4.741496401646282, 4.893568542229298, 5.048448422192488,
6969+ 5.20615066083972, 5.3666897647573375, 5.5300801301023865, 5.696336044816294,
7070+ 5.865471690767354, 6.037501145825082, 6.212438385869475, 6.390297286737924,
7171+ 6.571091626112461, 6.7548350853498045, 6.941541251256611, 7.131223617812143,
7272+ 7.323895587840543, 7.5195704746346665, 7.7182615035334345,
7373+ 7.919981813454504, 8.124744458384042, 8.332562408825165, 8.543448553206703,
7474+ 8.757415699253682, 8.974476575321063, 9.194643831691977, 9.417930041841839,
7575+ 9.644347703669503, 9.873909240696694, 10.106627003236781,
7676+ 10.342513269534024, 10.58158024687427, 10.8238400726681, 11.069304815507364,
7777+ 11.317986476196008, 11.569896988756009, 11.825048221409341,
7878+ 12.083451977536606, 12.345119996613247, 12.610063955123938,
7979+ 12.878295467455942, 13.149826086772048, 13.42466730586372,
8080+ 13.702830557985108, 13.984327217668513, 14.269168601521828,
8181+ 14.55736596900856, 14.848930523210871, 15.143873411576273,
8282+ 15.44220572664832, 15.743938506781891, 16.04908273684337, 16.35764934889634,
8383+ 16.66964922287304, 16.985093187232053, 17.30399201960269, 17.62635644741625,
8484+ 17.95219714852476, 18.281524751807332, 18.614349837764564,
8585+ 18.95068293910138, 19.290534541298456, 19.633915083172692,
8686+ 19.98083495742689, 20.331304511189067, 20.685334046541502,
8787+ 21.042933821039977, 21.404114048223256, 21.76888489811322,
8888+ 22.137256497705877, 22.50923893145328, 22.884842241736916,
8989+ 23.264076429332462, 23.6469514538663, 24.033477234264016, 24.42366364919083,
9090+ 24.817520537484558, 25.21505769858089, 25.61628489293138,
9191+ 26.021211842414342, 26.429848230738664, 26.842203703840827,
9292+ 27.258287870275353, 27.678110301598522, 28.10168053274597,
9393+ 28.529008062403893, 28.96010235337422, 29.39497283293396, 29.83362889318845,
9494+ 30.276079891419332, 30.722335150426627, 31.172403958865512,
9595+ 31.62629557157785, 32.08401920991837, 32.54558406207592, 33.010999283389665,
9696+ 33.4802739966603, 33.953417292456834, 34.430438229418264,
9797+ 34.911345834551085, 35.39614910352207, 35.88485700094671, 36.37747846067349,
9898+ 36.87402238606382, 37.37449765026789, 37.87891309649659, 38.38727753828926,
9999+ 38.89959975977785, 39.41588851594697, 39.93615253289054, 40.460400508064545,
100100+ 40.98864111053629, 41.520882981230194, 42.05713473317016,
101101+ 42.597404951718396, 43.141702194811224, 43.6900349931913, 44.24241185063697,
102102+ 44.798841244188324, 45.35933162437017, 45.92389141541209, 46.49252901546552,
103103+ 47.065252796817916, 47.64207110610409, 48.22299226451468,
104104+ 48.808024568002054, 49.3971762874833, 49.9904556690408, 50.587870934119984,
105105+ 51.189430279724725, 51.79514187861014, 52.40501387947288, 53.0190544071392,
106106+ 53.637271562750364, 54.259673423945976, 54.88626804504493,
107107+ 55.517063457223934, 56.15206766869424, 56.79128866487574, 57.43473440856916,
108108+ 58.08241284012621, 58.734331877617365, 59.39049941699807, 60.05092333227251,
109109+ 60.715611475655585, 61.38457167773311, 62.057811747619894, 62.7353394731159,
110110+ 63.417162620860914, 64.10328893648692, 64.79372614476921, 65.48848194977529,
111111+ 66.18756403501224, 66.89098006357258, 67.59873767827808, 68.31084450182222,
112112+ 69.02730813691093, 69.74813616640164, 70.47333615344107, 71.20291564160104,
113113+ 71.93688215501312, 72.67524319850172, 73.41800625771542, 74.16517879925733,
114114+ 74.9167682708136, 75.67278210128072, 76.43322770089146, 77.1981124613393,
115115+ 77.96744375590167, 78.74122893956174, 79.51947534912904, 80.30219030335869,
116116+ 81.08938110306934, 81.88105503125999, 82.67721935322541, 83.4778813166706,
117117+ 84.28304815182372, 85.09272707154808, 85.90692527145302, 86.72564993000343,
118118+ 87.54890820862819, 88.3767072518277, 89.2090541872801, 90.04595612594655,
119119+ 90.88742016217518, 91.73345337380438, 92.58406282226491, 93.43925555268066,
120120+ 94.29903859396902, 95.16341895893969, 96.03240364439274, 96.9059996312159,
121121+ 97.78421388448044, 98.6670533535366, 99.55452497210776,
122122+ ]
123123+124124+ /**
125125+ * Sanitizes a degree measure as a floating-point number.
126126+ *
127127+ * @returns A degree measure between 0.0 (inclusive) and 360.0 (exclusive).
128128+ */
129129+ static sanitizeDegreesDouble(degrees: number) {
130130+ degrees = degrees % 360.0
131131+ if (degrees < 0) {
132132+ degrees = degrees + 360.0
133133+ }
134134+ return degrees
135135+ }
136136+137137+ /** Equation used in CAM16 conversion that removes the effect of chromatic adaptation. */
138138+ static inverseChromaticAdaptation(adapted: number) {
139139+ const adaptedAbs = Math.abs(adapted)
140140+ const base = Math.max(0, (27.13 * adaptedAbs) / (400 - adaptedAbs))
141141+142142+ return CamUtils.signum(adapted) * Math.pow(base, 1 / 0.42)
143143+ }
144144+145145+ /**
146146+ * Finds a color with the given hue, chroma, and Y.
147147+ *
148148+ * @param hueRadians The desired hue in radians.
149149+ * @param chroma The desired chroma.
150150+ * @param y The desired Y.
151151+ * @returns The desired color as a hexadecimal integer, if found; 0 otherwise.
152152+ */
153153+ static findResultByJ(hueRadians: number, chroma: number, y: number) {
154154+ // Initial estimate of j.
155155+ let j = Math.sqrt(y) * 11.0
156156+157157+ // ===========================================================
158158+ // Operations inlined from Cam16 to avoid repeated calculation
159159+ // ===========================================================
160160+ const viewingConditions = Frame.DEFAULT,
161161+ tInnerCoeff =
162162+ 1 / Math.pow(1.64 - Math.pow(0.29, viewingConditions.getN()), 0.73),
163163+ eHue = 0.25 * (Math.cos(hueRadians + 2.0) + 3.8),
164164+ p1 =
165165+ eHue *
166166+ (50000 / 13) *
167167+ viewingConditions.getNc() *
168168+ viewingConditions.getNcb(),
169169+ hSin = Math.sin(hueRadians),
170170+ hCos = Math.cos(hueRadians)
171171+172172+ for (let iterationRound = 0; iterationRound < 5; iterationRound++) {
173173+ // ===========================================================
174174+ // Operations inlined from Cam16 to avoid repeated calculation
175175+ // ===========================================================
176176+ const jNormalized = j / 100.0,
177177+ alpha = chroma === 0 || j === 0 ? 0 : chroma / Math.sqrt(jNormalized),
178178+ t = Math.pow(alpha * tInnerCoeff, 1 / 0.9),
179179+ acExponent = 1 / viewingConditions.getC() / viewingConditions.getZ(),
180180+ ac = viewingConditions.getAw() * Math.pow(jNormalized, acExponent),
181181+ p2 = ac / viewingConditions.getNbb(),
182182+ gamma =
183183+ (23 * (p2 + 0.305) * t) / (23 * p1 + 11 * t * hCos + 108 * t * hSin),
184184+ a = gamma * hCos,
185185+ b = gamma * hSin,
186186+ rA = (460 * p2 + 451 * a + 288 * b) / 1403.0,
187187+ gA = (460 * p2 - 891 * a - 261 * b) / 1403.0,
188188+ bA = (460 * p2 - 220 * a - 6300 * b) / 1403.0,
189189+ rCScaled = HctSolver.inverseChromaticAdaptation(rA),
190190+ gCScaled = HctSolver.inverseChromaticAdaptation(gA),
191191+ bCScaled = HctSolver.inverseChromaticAdaptation(bA),
192192+ matrix = HctSolver.LINRGB_FROM_SCALED_DISCOUNT,
193193+ linrgbR =
194194+ rCScaled * matrix[0][0] +
195195+ gCScaled * matrix[0][1] +
196196+ bCScaled * matrix[0][2],
197197+ linrgbG =
198198+ rCScaled * matrix[1][0] +
199199+ gCScaled * matrix[1][1] +
200200+ bCScaled * matrix[1][2],
201201+ linrgbB =
202202+ rCScaled * matrix[2][0] +
203203+ gCScaled * matrix[2][1] +
204204+ bCScaled * matrix[2][2]
205205+206206+ // ===========================================================
207207+ // Operations inlined from Cam16 to avoid repeated calculation
208208+ // ===========================================================
209209+ if (linrgbR < 0 || linrgbG < 0 || linrgbB < 0) {
210210+ return 0
211211+ }
212212+213213+ const kR = HctSolver.Y_FROM_LINRGB[0],
214214+ kG = HctSolver.Y_FROM_LINRGB[1],
215215+ kB = HctSolver.Y_FROM_LINRGB[2],
216216+ fnj = kR * linrgbR + kG * linrgbG + kB * linrgbB
217217+218218+ if (fnj <= 0) {
219219+ return 0
220220+ }
221221+ if (iterationRound === 4 || Math.abs(fnj - y) < 0.002) {
222222+ if (linrgbR > 100.01 || linrgbG > 100.01 || linrgbB > 100.01) {
223223+ return 0
224224+ }
225225+ return CamUtils.argbFromLinrgbComponents(linrgbR, linrgbG, linrgbB)
226226+ }
227227+228228+ // Iterates with Newton method,
229229+ // Using 2 * fn(j) / j as the approximation of fn'(j)
230230+ j = j - ((fnj - y) * j) / (2 * fnj)
231231+ }
232232+233233+ return 0
234234+ }
235235+236236+ /**
237237+ * Finds an sRGB color with the given hue, chroma, and L*, if possible.
238238+ *
239239+ * @param hueDegrees The desired hue, in degrees.
240240+ * @param chroma The desired chroma.
241241+ * @param lstar The desired L*.
242242+ * @returns A hexadecimal representing the sRGB color. The color has sufficiently close hue, chroma, and L* to the desired
243243+ * values, if possible; otherwise, the hue and L* will be sufficiently close, and chroma will be maximized.
244244+ */
245245+ static solveToInt(hueDegrees: number, chroma: number, lstar: number) {
246246+ if (chroma < 0.0001 || lstar < 0.0001 || lstar > 99.9999) {
247247+ return CamUtils.argbFromLstar(lstar)
248248+ }
249249+250250+ hueDegrees = HctSolver.sanitizeDegreesDouble(hueDegrees)
251251+ const hueRadians = MathUtils.toRadians(hueDegrees)
252252+ const y = CamUtils.yFromLstar(lstar)
253253+254254+ const exactAnswer = HctSolver.findResultByJ(hueRadians, chroma, y)
255255+256256+ if (exactAnswer !== 0) {
257257+ return exactAnswer
258258+ }
259259+260260+ return HctSolver.bisectToLimit(y, hueRadians)
261261+ }
262262+263263+ /** Ensure X is between 0 and 100. */
264264+ static isBounded(x: number) {
265265+ return x >= 0 && x <= 100
266266+ }
267267+268268+ /**
269269+ * Returns the nth possible vertex of the polygonal intersection.
270270+ *
271271+ * @param y The Y value of the plane.
272272+ * @param n The zero-based index of the point. 0 <= n <= 11.
273273+ * @returns The nth possible vertex of the polygonal intersection of the y plane and the RGB cube in linear RGB coordinates, if
274274+ * it exists. If the possible vertex lies outside of the cube, [-1.0, -1.0, -1.0] is returned.
275275+ */
276276+ static nthVertex(y: number, n: number): [number, number, number] {
277277+ const kR = HctSolver.Y_FROM_LINRGB[0],
278278+ kG = HctSolver.Y_FROM_LINRGB[1],
279279+ kB = HctSolver.Y_FROM_LINRGB[2],
280280+ coordA = n % 4 <= 1 ? 0 : 100,
281281+ coordB = n % 2 === 0 ? 0 : 100
282282+283283+ if (n < 4) {
284284+ const g = coordA
285285+ const b = coordB
286286+ const r = (y - g * kG - b * kB) / kR
287287+ if (HctSolver.isBounded(r)) {
288288+ return [r, g, b]
289289+ } else {
290290+ return [-1.0, -1.0, -1.0]
291291+ }
292292+ } else if (n < 8) {
293293+ const b = coordA
294294+ const r = coordB
295295+ const g = (y - r * kR - b * kB) / kG
296296+ if (HctSolver.isBounded(g)) {
297297+ return [r, g, b]
298298+ } else {
299299+ return [-1.0, -1.0, -1.0]
300300+ }
301301+ } else {
302302+ const r = coordA
303303+ const g = coordB
304304+ const b = (y - r * kR - g * kG) / kB
305305+ if (HctSolver.isBounded(b)) {
306306+ return [r, g, b]
307307+ } else {
308308+ return [-1.0, -1.0, -1.0]
309309+ }
310310+ }
311311+ }
312312+313313+ static chromaticAdaptation(component: number) {
314314+ const af = Math.pow(Math.abs(component), 0.42)
315315+ return (CamUtils.signum(component) * 400 * af) / (af + 27.13)
316316+ }
317317+318318+ /**
319319+ * Returns the hue of a linear RGB color in CAM16.
320320+ *
321321+ * @param linrgb The linear RGB coordinates of a color.
322322+ * @returns The hue of the color in CAM16, in radians.
323323+ */
324324+ static hueOf(linrgb: [number, number, number]) {
325325+ // Calculate scaled discount components using in-lined matrix multiplication to avoid
326326+ // an array allocation.
327327+ const matrix = HctSolver.SCALED_DISCOUNT_FROM_LINRGB,
328328+ row = linrgb,
329329+ rD =
330330+ linrgb[0] * matrix[0][0] +
331331+ row[1] * matrix[0][1] +
332332+ row[2] * matrix[0][2],
333333+ gD =
334334+ linrgb[0] * matrix[1][0] +
335335+ row[1] * matrix[1][1] +
336336+ row[2] * matrix[1][2],
337337+ bD =
338338+ linrgb[0] * matrix[2][0] + row[1] * matrix[2][1] + row[2] * matrix[2][2]
339339+340340+ const rA = HctSolver.chromaticAdaptation(rD),
341341+ gA = HctSolver.chromaticAdaptation(gD),
342342+ bA = HctSolver.chromaticAdaptation(bD)
343343+344344+ // redness-greenness
345345+ const a = (11 * rA + -12 * gA + bA) / 11
346346+347347+ // yellowness-blueness
348348+ const b = (rA + gA - 2 * bA) / 9
349349+350350+ return Math.atan2(b, a)
351351+ }
352352+353353+ /**
354354+ * Sanitizes a small enough angle in radians.
355355+ *
356356+ * @param angle An angle in radians; must not deviate too much from 0.
357357+ * @returns A coterminal angle between 0 and 2pi.
358358+ */
359359+ static sanitizeRadians(angle: number) {
360360+ return (angle + Math.PI * 8) % (Math.PI * 2)
361361+ }
362362+363363+ /**
364364+ * Cyclic order is the idea that 330° → 5° → 200° is in order, but, 180° → 270° → 210° is not. Visually, A B and C are angles,
365365+ * and they are in cyclic order if travelling from A to C in a way that increases angle (ex. counter-clockwise if +x axis = 0
366366+ * degrees and +y = 90) means you must cross B.
367367+ *
368368+ * @param a First angle in possibly cyclic triplet
369369+ * @param b Second angle in possibly cyclic triplet
370370+ * @param c Third angle in possibly cyclic triplet
371371+ * @returns True if B is between A and C
372372+ */
373373+ static areInCyclicOrder(a: number, b: number, c: number) {
374374+ const deltaAB = HctSolver.sanitizeRadians(b - a)
375375+ const deltaAC = HctSolver.sanitizeRadians(c - a)
376376+377377+ return deltaAB < deltaAC
378378+ }
379379+380380+ /**
381381+ * Finds the segment containing the desired color.
382382+ *
383383+ * @param y The Y value of the color.
384384+ * @param targetHue The hue of the color.
385385+ * @returns A list of two sets of linear RGB coordinates, each corresponding to an endpoint of the segment containing the
386386+ * desired color.
387387+ */
388388+ static bisectToSegment(y: number, targetHue: number) {
389389+ let left: [number, number, number] = [-1.0, -1.0, -1.0],
390390+ right = left,
391391+ leftHue = 0.0,
392392+ rightHue = 0.0,
393393+ initialized = false,
394394+ uncut = true
395395+396396+ for (let n = 0; n < 12; n++) {
397397+ const mid = HctSolver.nthVertex(y, n)
398398+ if (mid[0] < 0) {
399399+ continue
400400+ }
401401+402402+ const midHue = HctSolver.hueOf(mid)
403403+ if (!initialized) {
404404+ left = mid
405405+ right = mid
406406+ leftHue = midHue
407407+ rightHue = midHue
408408+ initialized = true
409409+ continue
410410+ }
411411+412412+ if (uncut || HctSolver.areInCyclicOrder(leftHue, midHue, rightHue)) {
413413+ uncut = false
414414+ if (HctSolver.areInCyclicOrder(leftHue, targetHue, midHue)) {
415415+ right = mid
416416+ rightHue = midHue
417417+ } else {
418418+ left = mid
419419+ leftHue = midHue
420420+ }
421421+ }
422422+ }
423423+424424+ return [left, right] as [[number, number, number], [number, number, number]]
425425+ }
426426+427427+ static criticalPlaneBelow(x: number) {
428428+ return Math.floor(x - 0.5)
429429+ }
430430+431431+ static criticalPlaneAbove(x: number) {
432432+ return Math.ceil(x - 0.5)
433433+ }
434434+435435+ /**
436436+ * Delinearizes an RGB component, returning a floating-point number.
437437+ *
438438+ * @param rgbComponent 0.0 <= rgb_component <= 100.0, represents linear R/G/B channel
439439+ * @returns 0.0 <= output <= 255.0, color channel converted to regular RGB space
440440+ */
441441+ static trueDelinearized(rgbComponent: number) {
442442+ const normalized = rgbComponent / 100.0
443443+444444+ let delinearized
445445+ if (normalized <= 0.0031308) {
446446+ delinearized = normalized * 12.92
447447+ } else {
448448+ delinearized = 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055
449449+ }
450450+451451+ return delinearized * 255.0
452452+ }
453453+454454+ /**
455455+ * Find an intercept using linear interpolation.
456456+ *
457457+ * @param source The starting number.
458458+ * @param mid The number in the middle.
459459+ * @param target The ending number.
460460+ * @returns A number t such that lerp(source, target, t) = mid.
461461+ */
462462+ static intercept(source: number, mid: number, target: number) {
463463+ if (target === source) return target
464464+465465+ return (mid - source) / (target - source)
466466+ }
467467+468468+ /**
469469+ * Linearly interpolate between two points in three dimensions.
470470+ *
471471+ * @param source Three dimensions representing the starting point
472472+ * @param t The percentage to travel between source and target, from 0 to 1
473473+ * @param target Three dimensions representing the end point
474474+ * @returns Three dimensions representing the point t percent from source to target.
475475+ */
476476+ static lerpPoint(
477477+ source: [number, number, number],
478478+ t: number,
479479+ target: [number, number, number],
480480+ ) {
481481+ return [
482482+ source[0] + (target[0] - source[0]) * t,
483483+ source[1] + (target[1] - source[1]) * t,
484484+ source[2] + (target[2] - source[2]) * t,
485485+ ] as [number, number, number]
486486+ }
487487+488488+ /**
489489+ * Intersects a segment with a plane.
490490+ *
491491+ * @param source The coordinates of point A.
492492+ * @param coordinate The R-, G-, or B-coordinate of the plane.
493493+ * @param target The coordinates of point B.
494494+ * @param axis The axis the plane is perpendicular with. (0: R, 1: G, 2: B)
495495+ * @returns The intersection point of the segment AB with the plane R=coordinate, G=coordinate, or B=coordinate
496496+ */
497497+ static setCoordinate(
498498+ source: [number, number, number],
499499+ coordinate: number,
500500+ target: [number, number, number],
501501+ axis: 0 | 1 | 2,
502502+ ) {
503503+ const t = HctSolver.intercept(source[axis], coordinate, target[axis])
504504+505505+ return HctSolver.lerpPoint(source, t, target)
506506+ }
507507+508508+ /**
509509+ * Finds a color with the given Y and hue on the boundary of the cube.
510510+ *
511511+ * @param y The Y value of the color.
512512+ * @param targetHue The hue of the color.
513513+ * @returns The desired color, in linear RGB coordinates.
514514+ */
515515+ static bisectToLimit(y: number, targetHue: number) {
516516+ const segment = HctSolver.bisectToSegment(y, targetHue)
517517+518518+ let left = segment[0],
519519+ leftHue = HctSolver.hueOf(left),
520520+ right = segment[1]
521521+522522+ for (let axis = 0 as 0 | 1 | 2; axis < 3; axis++) {
523523+ if (left[axis] !== right[axis]) {
524524+ let lPlane = -1
525525+ let rPlane = 255
526526+ if (left[axis] < right[axis]) {
527527+ lPlane = HctSolver.criticalPlaneBelow(
528528+ HctSolver.trueDelinearized(left[axis]),
529529+ )
530530+ rPlane = HctSolver.criticalPlaneAbove(
531531+ HctSolver.trueDelinearized(right[axis]),
532532+ )
533533+ } else {
534534+ lPlane = HctSolver.criticalPlaneAbove(
535535+ HctSolver.trueDelinearized(left[axis]),
536536+ )
537537+ rPlane = HctSolver.criticalPlaneBelow(
538538+ HctSolver.trueDelinearized(right[axis]),
539539+ )
540540+ }
541541+ for (let i = 0; i < 8; i++) {
542542+ if (Math.abs(rPlane - lPlane) <= 1) {
543543+ break
544544+ } else {
545545+ const mPlane = Math.floor((lPlane + rPlane) / 2.0)
546546+ const midPlaneCoordinate = HctSolver.CRITICAL_PLANES[mPlane] ?? 0
547547+548548+ const mid: [number, number, number] = HctSolver.setCoordinate(
549549+ left,
550550+ midPlaneCoordinate,
551551+ right,
552552+ axis,
553553+ )
554554+ const midHue = HctSolver.hueOf(mid)
555555+ if (HctSolver.areInCyclicOrder(leftHue, targetHue, midHue)) {
556556+ right = mid
557557+ rPlane = mPlane
558558+ } else {
559559+ left = mid
560560+ leftHue = midHue
561561+ lPlane = mPlane
562562+ }
563563+ }
564564+ }
565565+ }
566566+ }
567567+568568+ return CamUtils.argbFromLinrgbComponents(
569569+ (left[0] + right[0]) / 2,
570570+ (left[1] + right[1]) / 2,
571571+ (left[2] + right[2]) / 2,
572572+ )
573573+ }
574574+}
···11+/*
22+ * This code was originally written in Java and has been converted to JavaScript.
33+ *
44+ * Original Java code source: [https://android.googlesource.com/platform/frameworks/base/+/6844741fb8351f3aa82b96ce64a1bd83ea7989bd/packages/SystemUI/monet/src/com/android/systemui/monet?autodive=0%2F]
55+ *
66+ * Copyright (C) 2021 The Android Open Source Project.
77+ * The original code is licensed under the Apache License, Version 2.0.
88+ *
99+ * This JavaScript version is licensed under the MIT License.
1010+ *
1111+ * See the respective licenses for the specific terms, permissions, and limitations.
1212+ */
1313+1414+import ColorUtils from '../Utils/ColorUtils'
1515+1616+export default class Shades {
1717+ /**
1818+ * Combining the ability to convert between relative luminance and perceptual luminance with contrast leads to a design system
1919+ * that can be based on a linear value to determine contrast, rather than a ratio.
2020+ *
2121+ * This codebase implements a design system that has that property, and as a result, we can guarantee that any shades 5 steps
2222+ * from each other have a contrast ratio of at least 4.5. 4.5 is the requirement for smaller text contrast in WCAG 2.1 and
2323+ * earlier.
2424+ *
2525+ * However, lstar 50 does _not_ have a contrast ratio >= 4.5 with lstar 100. lstar 49.6 is the smallest lstar that will lead to
2626+ * a contrast ratio >= 4.5 with lstar 100, and it also contrasts >= 4.5 with lstar 100.
2727+ */
2828+ static MIDDLE_LSTAR = 49.6
2929+3030+ /**
3131+ * Generate shades of a color. Ordered in lightness _descending_. The first shade will be at 95% lightness, the next at 90, 80,
3232+ * etc. through 0.
3333+ *
3434+ * @param hue Hue in CAM16 color space
3535+ * @param chroma Chroma in CAM16 color space
3636+ * @returns Shades of a color, as argb integers. Ordered by lightness descending.
3737+ */
3838+ static of(hue: number, chroma: number) {
3939+ const shades: number[] = []
4040+4141+ // At tone 90 and above, blue and yellow hues can reach a much higher chroma.
4242+ // To preserve a consistent appearance across all hues, use a maximum chroma of 40.
4343+ shades[0] = ColorUtils.CAMToColor(hue, Math.min(40, chroma), 99)
4444+ shades[1] = ColorUtils.CAMToColor(hue, Math.min(40, chroma), 95)
4545+4646+ for (let i = 2; i < 12; i++) {
4747+ const lStar = i === 6 ? Shades.MIDDLE_LSTAR : 100 - 10 * (i - 1)
4848+ if (lStar >= 90) {
4949+ chroma = Math.min(40, chroma)
5050+ }
5151+ shades[i] = ColorUtils.CAMToColor(hue, chroma, lStar)
5252+ }
5353+5454+ shades.unshift(16777215) // first color is always pure white
5555+5656+ return shades
5757+ }
5858+}
+106
src/alf/util/material3/Types.ts
···11+type ShadesArr = [
22+ string,
33+ string,
44+ string,
55+ string,
66+ string,
77+ string,
88+ string,
99+ string,
1010+ string,
1111+ string,
1212+ string,
1313+ string,
1414+ string,
1515+]
1616+1717+export type MaterialYouPalette = {
1818+ /** An array with `13` shades. */
1919+ system_accent1: ShadesArr
2020+ /** An array with `13` shades. */
2121+ system_accent2: ShadesArr
2222+ /** An array with `13` shades. */
2323+ system_accent3: ShadesArr
2424+ /** An array with `13` shades. */
2525+ system_neutral1: ShadesArr
2626+ /** An array with `13` shades. */
2727+ system_neutral2: ShadesArr
2828+}
2929+3030+export type GenerationStyle =
3131+ | 'SPRITZ'
3232+ | 'TONAL_SPOT'
3333+ | 'VIBRANT'
3434+ | 'EXPRESSIVE'
3535+ | 'RAINBOW'
3636+ | 'FRUIT_SALAD'
3737+ | 'CONTENT'
3838+ | 'MONOCHROMATIC'
3939+4040+export type ColorScheme = 'light' | 'dark' | 'auto'
4141+4242+export type MapPaletteToThemeType = (palette: MaterialYouPalette) => {
4343+ light: Record<string, unknown>
4444+ dark: Record<string, unknown>
4545+}
4646+4747+export type ThemeProviderProps = {
4848+ /**
4949+ * Specifies the initial color scheme for your app.
5050+ *
5151+ * `"auto" | "dark" | "light"`
5252+ */
5353+ colorScheme?: ColorScheme
5454+ /**
5555+ * This is used to generate a fallback palette in case the platform does not support Material You colors.
5656+ *
5757+ * **Note:** provide a color only in HEX format
5858+ */
5959+ fallbackColor?: string
6060+ /**
6161+ * If set to "auto", it tries to get the palette from the device, falling back to the provided color if unsupported. If set to a
6262+ * color (HEX only), it generates a new palette without device retrieval.
6363+ */
6464+ seedColor?: 'auto' | (string & NonNullable<unknown>)
6565+ /**
6666+ * Palette generation style. The style that dictates how the palette will be generated.
6767+ *
6868+ * `"SPRITZ"| "TONAL_SPOT"| "VIBRANT"| "EXPRESSIVE"| "RAINBOW"| "FRUIT_SALAD"| "CONTENT"| "MONOCHROMATIC"`
6969+ */
7070+ generationStyle?: GenerationStyle
7171+ children?: React.ReactNode
7272+}
7373+7474+export type MaterialYouThemeContext = {
7575+ /** Switch between themes (`dark` or `light`) or set to `auto` to follow system color scheme preference. */
7676+ setColorScheme: (value: ColorScheme) => void
7777+ /**
7878+ * Generate a new Material You palette and set it as the current theme.
7979+ *
8080+ * @param {string} seed
8181+ *
8282+ * - The seed color. It can be `"auto"` to follow the system theme if supported; otherwise, it will generate a palette using the
8383+ * `fallbackColor` prop. If a HEX color is provided, it will generate a new palette using that seed color.
8484+ *
8585+ * @param {string} style - The style that dictates how the palette will be generated.
8686+ */
8787+ setMaterialYouColor: (
8888+ seed: 'auto' | (string & NonNullable<unknown>),
8989+ style?: GenerationStyle,
9090+ ) => void
9191+ /**
9292+ * Change the palette generation style and set it as the current theme.
9393+ *
9494+ * **Disclaimer**: If the current Material You palette is set to `"auto"` (following the system theme), a new palette will be
9595+ * generated using the `fallbackColor` prop.
9696+ *
9797+ * @param {string} style - The style that dictates how the palette will be generated.
9898+ */
9999+ setPaletteStyle: (style: GenerationStyle) => void
100100+ /** The current seed color used to generate the palette. If the palette follows the system theme, it will be `"auto"`. */
101101+ seedColor: 'auto' | (string & NonNullable<unknown>)
102102+ /** The current generation style used to generate the palette */
103103+ style: GenerationStyle
104104+ /** The current palette. */
105105+ palette: MaterialYouPalette
106106+}
···11+import {Cam} from '../Cam/Cam'
22+import Color from './Color'
33+44+export default class ColorUtils {
55+ static XYZ_WHITE_REFERENCE_X = 95.047
66+ static XYZ_WHITE_REFERENCE_Y = 100
77+ static XYZ_WHITE_REFERENCE_Z = 108.883
88+99+ static constrain(amount: number, low: number, high: number) {
1010+ return amount < low ? low : amount > high ? high : amount
1111+ }
1212+1313+ /**
1414+ * Converts a color from CIE XYZ to its RGB representation.
1515+ *
1616+ * This method expects the XYZ representation to use the D65 illuminant and the CIE 2° Standard Observer (1931).
1717+ *
1818+ * @param x X component value [0...95.047)
1919+ * @param y Y component value [0...100)
2020+ * @param z Z component value [0...108.883)
2121+ * @returns Int containing the RGB representation
2222+ */
2323+ static XYZToColor(x: number, y: number, z: number) {
2424+ x = Color.clamp(x, 0, ColorUtils.XYZ_WHITE_REFERENCE_X)
2525+ y = Color.clamp(y, 0, ColorUtils.XYZ_WHITE_REFERENCE_Y)
2626+ z = Color.clamp(z, 0, ColorUtils.XYZ_WHITE_REFERENCE_Z)
2727+2828+ let r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100
2929+ let g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100
3030+ let b = (x * 0.0557 + y * -0.204 + z * 1.057) / 100
3131+3232+ r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r
3333+ g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g
3434+ b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b
3535+3636+ return Color.rgb(
3737+ ColorUtils.constrain(Math.round(r * 255), 0, 255),
3838+ ColorUtils.constrain(Math.round(g * 255), 0, 255),
3939+ ColorUtils.constrain(Math.round(b * 255), 0, 255),
4040+ )
4141+ }
4242+4343+ /**
4444+ * Convert a color appearance model representation to an ARGB color.
4545+ *
4646+ * Note: the returned color may have a lower chroma than requested. Whether a chroma is available depends on luminance. For
4747+ * example, there's no such thing as a high chroma light red, due to the limitations of our eyes and/or physics. If the
4848+ * requested chroma is unavailable, the highest possible chroma at the requested luminance is returned.
4949+ *
5050+ * @param hue Hue, in degrees, in CAM coordinates
5151+ * @param chroma Chroma in CAM coordinates.
5252+ * @param lstar Perceptual luminance, L* in L_a_b*
5353+ */
5454+ static CAMToColor(hue: number, chroma: number, lstar: number) {
5555+ return Cam.getInt(hue, chroma, lstar)
5656+ }
5757+}
···11+// https://github.com/alabsi91/react-native-material-you-colors
22+/*!
33+MIT License
44+55+Copyright (c) 2023 Ahmed ALABSI
66+Permission is hereby granted, free of charge, to any person obtaining a copy
77+of this software and associated documentation files (the "Software"), to deal
88+in the Software without restriction, including without limitation the rights
99+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1010+copies of the Software, and to permit persons to whom the Software is
1111+furnished to do so, subject to the following conditions:
1212+1313+The above copyright notice and this permission notice shall be included in all
1414+copies or substantial portions of the Software.
1515+1616+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1717+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1818+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1919+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2020+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2121+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2222+SOFTWARE.
2323+*/
2424+2525+import Palette from './Monet/Palette'
2626+import {type GenerationStyle} from './Types'
2727+2828+export type {GenerationStyle}
2929+export const generatePaletteFromColor = (
3030+ colorSeed: string,
3131+ style?: GenerationStyle,
3232+) => Palette.generate(colorSeed, style)