| 1 | """Recolour a palette by decomposing each pixel into a blend of two anchors. |
| 2 | |
| 3 | The upstream assets antialias by blending between two theme colours rather than |
| 4 | by alpha alone -- a checkmark, for instance, fades from the accent fill to |
| 5 | white across two pixels. Mapping colours one at a time would leave those |
| 6 | gradients stranded between the old palette and the new. |
| 7 | |
| 8 | So for each input colour we find the anchor pair (a, b) and the blend factor t |
| 9 | that best explain it, then emit the same blend of the mapped anchors (a', b'). |
| 10 | Exact anchors fall out as the t=0 and t=1 cases. |
| 11 | """ |
| 12 | |
| 13 | |
| 14 | def _sub(p, q): |
| 15 | return (p[0] - q[0], p[1] - q[1], p[2] - q[2]) |
| 16 | |
| 17 | |
| 18 | def _dot(p, q): |
| 19 | return p[0] * q[0] + p[1] * q[1] + p[2] * q[2] |
| 20 | |
| 21 | |
| 22 | class Remapper: |
| 23 | def __init__(self, anchors): |
| 24 | """anchors: list of (source_rgb, target_rgb) tuples.""" |
| 25 | self.anchors = list(anchors) |
| 26 | self._cache = {} |
| 27 | |
| 28 | def __call__(self, color): |
| 29 | hit = self._cache.get(color) |
| 30 | if hit is None: |
| 31 | hit = self._cache[color] = self._solve(color) |
| 32 | return hit |
| 33 | |
| 34 | def _solve(self, c): |
| 35 | best_err = None |
| 36 | best = None |
| 37 | for i, (a_src, a_dst) in enumerate(self.anchors): |
| 38 | for b_src, b_dst in self.anchors[i:]: |
| 39 | v = _sub(b_src, a_src) |
| 40 | vv = _dot(v, v) |
| 41 | if vv == 0: |
| 42 | t = 0.0 |
| 43 | else: |
| 44 | t = _dot(_sub(c, a_src), v) / vv |
| 45 | t = 0.0 if t < 0 else (1.0 if t > 1 else t) |
| 46 | proj = (a_src[0] + t * v[0], a_src[1] + t * v[1], a_src[2] + t * v[2]) |
| 47 | d = _sub(c, proj) |
| 48 | err = _dot(d, d) |
| 49 | if best_err is None or err < best_err: |
| 50 | best_err = err |
| 51 | best = tuple( |
| 52 | a_dst[k] + t * (b_dst[k] - a_dst[k]) for k in range(3) |
| 53 | ) |
| 54 | return tuple( |
| 55 | 0 if v < 0 else (255 if v > 255 else v) |
| 56 | for v in (int(round(x)) for x in best) |
| 57 | ) |