|
| 1 | +""" |
| 2 | +Handle spectral related things. |
| 3 | +
|
| 4 | +- Calculations colors from/to wavelengths. |
| 5 | +""" |
| 6 | +from __future__ import annotations |
| 7 | +import math |
| 8 | +from . import algebra as alg |
| 9 | +from . import util |
| 10 | +from .types import Vector, VectorLike |
| 11 | +from . import cat |
| 12 | +from . import cmfs |
| 13 | + |
| 14 | +WHITE = cat.WHITES['2deg']['E'] |
| 15 | +LOCUS_START = 360 |
| 16 | +LOCUS_END = 780 |
| 17 | +LOCUS_STEP = 1 |
| 18 | + |
| 19 | + |
| 20 | +def xy_to_angle(xy: VectorLike, white: VectorLike, offset: float = 0.0, invert: bool = False) -> float: |
| 21 | + """ |
| 22 | + Translate xy to an angle with white being the origin. |
| 23 | +
|
| 24 | + If offset is provided, make the angle relative to the offset angle. |
| 25 | +
|
| 26 | + If invert is requested, we want to rotate the xy point 180 degrees. |
| 27 | + """ |
| 28 | + |
| 29 | + norm = alg.subtract(xy, white, dims=alg.D1) |
| 30 | + if invert: |
| 31 | + norm = alg.multiply(norm, -1, dims=alg.D1_SC) |
| 32 | + if offset: |
| 33 | + angle = (alg.rect_to_polar(*norm)[1] - offset) % 360 |
| 34 | + if angle < 1e-12: |
| 35 | + angle = 360.0 |
| 36 | + else: |
| 37 | + angle = alg.rect_to_polar(*norm)[1] |
| 38 | + return angle |
| 39 | + |
| 40 | + |
| 41 | +def closest_wavelength( |
| 42 | + xy: VectorLike, |
| 43 | + white: VectorLike = WHITE, |
| 44 | + reverse: bool = False, |
| 45 | + closest: bool = True |
| 46 | +) -> tuple[float, Vector, Vector]: |
| 47 | + """ |
| 48 | + Get the closest dominant wavelength. |
| 49 | +
|
| 50 | + Both intersections are returned, even if the other is on the line of purple. |
| 51 | + The first point is always in the dominant direction. If the dominant cannot |
| 52 | + be found, the complementary will be used and indicated with a negative sign. |
| 53 | +
|
| 54 | + If `reverse` is set, then the complementary wavelength is returned instead, |
| 55 | + with the points arranged favoring the complementary wavelength. If it cannot |
| 56 | + be found, the dominant wavelength is returned with a negative sign. |
| 57 | +
|
| 58 | + If `closet` is set, wavelengths are rounded to the closest. |
| 59 | + """ |
| 60 | + |
| 61 | + w1 = w2 = math.nan |
| 62 | + dominant = [math.nan, math.nan] |
| 63 | + complementary = [math.nan, math.nan] |
| 64 | + |
| 65 | + # Achromatic, no wavelength |
| 66 | + if all(abs(a - b) < 1e-12 for a, b in zip(xy, white)): |
| 67 | + return w1, dominant, complementary |
| 68 | + |
| 69 | + # Look for first intersection of the line drawn through the white point |
| 70 | + # and the current color with the spectral locus. Check the dominant and |
| 71 | + # complementary, but return as soon as we have the dominant. If no dominant |
| 72 | + # is found, we'll use the complementary. |
| 73 | + locus = [util.xyz_to_xyY(cmfs.CIE_1931_2DEG[r], white)[:-1] for r in range(LOCUS_START, LOCUS_END + 1, LOCUS_STEP)] |
| 74 | + start = xy_to_angle(locus[0], white) |
| 75 | + current = xy_to_angle(xy, white, start) |
| 76 | + invert = xy_to_angle(xy, white, start, invert=True) |
| 77 | + found = [False, False] |
| 78 | + for i in range(1, len(locus) - 2): |
| 79 | + # Get the next locus point angle |
| 80 | + a_next = xy_to_angle(locus[i], white, start) |
| 81 | + |
| 82 | + # Check if our angle is greater than the current locus point's angle |
| 83 | + for j in range(0, 2): |
| 84 | + |
| 85 | + # If has already been found, skip |
| 86 | + target = invert if j else current |
| 87 | + if a_next > target or found[j]: |
| 88 | + continue |
| 89 | + |
| 90 | + # Previous index |
| 91 | + i0 = i - 1 |
| 92 | + |
| 93 | + # Get the intersection |
| 94 | + if target == a_next: |
| 95 | + intersect = locus[i] # type: Vector | None |
| 96 | + elif target == xy_to_angle(locus[i0], white, start): |
| 97 | + intersect = locus[i0] |
| 98 | + else: |
| 99 | + intersect = alg.ray_line_intersect(white, xy, locus[i0], locus[i]) |
| 100 | + if intersect is None: # pragma: no cover |
| 101 | + continue |
| 102 | + |
| 103 | + # Use the intersection to estimate the interpolation factor for the wavelength, |
| 104 | + # and then get the interpolated value via Sprague interpolation. |
| 105 | + i2 = 0 if abs(locus[i0][0] - locus[i][0]) > abs(locus[i0][1] - locus[i][1]) else 1 |
| 106 | + f = alg.ilerp(locus[i0][i2], locus[i][i2], intersect[i2]) |
| 107 | + w = alg.lerp(LOCUS_START + i0, LOCUS_START + i, f) |
| 108 | + intersect = util.xyz_to_xyY(cmfs.CIE_1931_2DEG[w], white)[:-1] |
| 109 | + |
| 110 | + if j == 0: |
| 111 | + dominant = intersect |
| 112 | + w1 = w |
| 113 | + found[j] = True |
| 114 | + if not reverse: |
| 115 | + break |
| 116 | + else: |
| 117 | + complementary = intersect |
| 118 | + w2 = w |
| 119 | + found[j] = True |
| 120 | + if reverse: |
| 121 | + break |
| 122 | + |
| 123 | + if found[reverse]: |
| 124 | + break |
| 125 | + |
| 126 | + # Unlikely catastrophic failure |
| 127 | + if not any(found): # pragma: no cover |
| 128 | + return w1, dominant, complementary |
| 129 | + |
| 130 | + # Swap dominant and complementary if we are looking for complementary |
| 131 | + if reverse: |
| 132 | + dominant, complementary = complementary, dominant |
| 133 | + w1, w2 = w2, w1 |
| 134 | + |
| 135 | + # If dominant isn't found, it is on the line of purples; use complementary instead |
| 136 | + if not found[reverse]: |
| 137 | + pt = alg.ray_line_intersect(white, xy, locus[0], locus[-1]) |
| 138 | + # Shouldn't happen, but just in case |
| 139 | + if pt is not None: # pragma: no cover |
| 140 | + dominant = pt |
| 141 | + w1 = -alg.round_half_up(w2) if closest else -w2 |
| 142 | + else: |
| 143 | + complementary = dominant |
| 144 | + if closest: |
| 145 | + w1 = alg.round_half_up(w1) |
| 146 | + |
| 147 | + return w1, dominant, complementary |
| 148 | + |
| 149 | + |
| 150 | +def wavelength_to_color(wavelength: float) -> Vector: |
| 151 | + """Return the XYZ value for the specified wavelength.""" |
| 152 | + |
| 153 | + if wavelength < LOCUS_START or wavelength > LOCUS_END: |
| 154 | + raise ValueError(f'{wavelength}nm exceeds the range of {LOCUS_START}nm - {LOCUS_END}nm') |
| 155 | + |
| 156 | + # Wavelength is within the CMFs |
| 157 | + return cmfs.CIE_1931_2DEG[wavelength] |
0 commit comments