src/lib/remap.py 2.0 K raw
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
        )