Yeah sweet - just clarifying that you didn't want to incorporate a transition into the solution.
– ZzeMar 23 '17 at 3:52

1

May be a blend mode would work for you ? You can easily convert black to any color ... But I don't get the global picture of what you want to achieve
– valsMay 13 '17 at 20:13

1

@glebm so you need to find a formula (using any method) to turn black into any color and apply it using css ?
– ProllyGeekMay 14 '17 at 1:12

2

@ProllyGeek Yes. One other constraint I should mention is that the resulting formula cannot be a brute force lookup of a 5GiB table (it should be usable from e.g. javascript on a webpage).
– glebmMay 14 '17 at 1:15

6 Answers
6

@Dave was the first to post an answer to this (with working code), and his answer has been an invaluable source of shameless copy and pasting inspiration to me. This post began as an attempt to explain and refine @Dave's
answer, but it has since evolved into an answer of its own.

My method is significantly faster. According to a jsPerf benchmark on randomly generated RGB colors, @Dave's algorithm runs in 600 ms, while mine runs in 30 ms. This can definitely matter, for instance in load time, where speed is critical.

Implementing invert()

In the following, C is the initial component and C' is the remapped component; both in the closed interval [0,1].

For "table", the function is defined by linear interpolation between values given in the attribute tableValues. The table has n + 1 values (i.e., v0 to vn) specifying the start and end values for n evenly sized interpolation regions. Interpolations use the following formula:

For a value C find k such that:

k / n ≤ C < (k + 1) / n

The result C' is given by:

C' = vk + (C - k / n) * n * (vk+1 - vk)

An explanation of this formula:

The invert() filter defines this table: [value, 1 - value]. This is tableValues or v.

The formula defines n, such that n + 1 is the table's length. Since the table's length is 2, n = 1.

The formula defines k, with k and k + 1 being indexes of the table. Since the table has 2 elements, k = 0.

Thus, we can simplify the formula to:

C' = v0 + C * (v1 - v0)

Inlining the table's values, we are left with:

C' = value + C * (1 - value - value)

One more simplification:

C' = value + C * (1 - 2 * value)

The spec defines C and C' to be RGB values, within the bounds 0-1 (as opposed to 0-255). As a result, we must scale down the values before computation, and scale them back up after.

It then iterates through all computed colors. It stops once it has found a generated color within tolerance (all RGB values are within 5 units from the target color).

However, this is slow and inefficient. Thus, I present my own answer.

Implementing SPSA

First, we must define a loss function, that returns the difference between the color produced by a filter combination, and the target color. If the filters are perfect, the loss function should return 0.

Reusing all arrays (deltas, highArgs, lowArgs), instead of recreating them with each iteration.

Using an array of values for a, instead of a single value. This is because all of the filters are different, and thus they should move/converge at different speeds.

Running a fix function after each iteration. It clamps all values to between 0% and 100%, except saturate (where the maximum is 7500%), brightness and contrast (where the maximum is 200%), and hueRotate (where the values are wrapped around instead of clamped).

I use SPSA in a two-stage process:

The "wide" stage, that tries to "explore" the search space. It will make limited retries of SPSA if the results are not satisfactory.

The "narrow" stage, that takes the best result from the wide stage and attempts to "refine" it. It uses dynamic values for A and a.

Tuning SPSA

Warning: Do not mess with the SPSA code, especially with its constants, unless you are sure you know what you are doing.

The important constants are A, a, c, the initial values, the retry thresholds, the values of max in fix(), and the number of iterations of each stage. All of these values were carefully tuned to produce good results, and randomly screwing with them will almost definitely reduce the usefulness of the algorithm.

Then run the code in Node.js. After quite some time, the result should be something like this:

Average loss: 3.4768521401985275
Average time: 11.4915ms

Now tune the constants to your heart's content.

Some tips:

The average loss should be around 4. If it is greater than 4, it is producing results that are too far off, and you should tune for accuracy. If it is less than 4, it is wasting time, and you should reduce the number of iterations.

If you increase/decrease the number of iterations, adjust A appropriately.

This is a completely insane method. You can set a color directly using a SVG filter (fifth column in a feColorMatrix) and you can reference that filter from CSS - why wouldn't you use that method?
– Michael MullanyDec 30 '18 at 2:00

1

@MichaelMullany Well, that's embarrassing for me, considering how long I worked on this. I didn't think of your method, but now I understand – to recolor an element to any arbitrary color, you just dynamically generate a SVG with a <filter> containing a <feColorMatrix> with the proper values (all zeroes except the last column, which contains the target RGB values, 0, and 1), insert the SVG into the DOM, and reference the filter from CSS. Please write up your solution as an answer (with a demo), and I'll upvote.
– MultiplyByZer0Dec 30 '18 at 4:54

EDIT: This solution is not intended for production use and only illustrates an approach that can be taken to achieve what OP is asking for. As is, it is weak in some areas of the color spectrum. Better results can be achieved by more granularity in the step iterations or by implementing more filter functions for reasons described in detail in @MultiplyByZer0's answer.

EDIT2: OP is looking for a non brute force solution. In that case it's pretty simple, just solve this equation:

You can make this all very simple by just using a SVG filter referenced from CSS. You only need a single feColorMatrix to do a recolor. This one recolors to yellow. The fifth column in the feColorMatrix holds the RGB target values on the unit scale. (for yellow - it's 1,1,0)

An interesting solution but it seems that it does not allow controlling the target color via CSS.
– glebmMar 3 '19 at 19:26

You have to define a new filter for each color you want to apply. But it's fully accurate. hue-rotate is an approximation that clips certain colors - meaning that you can't achieve certain colors accurately using it - as the answers above attest. What we really need is a recolor() CSS filter shorthand.
– Michael MullanyMar 4 '19 at 20:59

MultiplyByZer0's answer calculates a series of filters that achieve with very high accuracy, without modifying HTML. A true hue-rotate in browsers would be nice yeah.
– glebmMar 8 '19 at 2:02

2

it seems this only produces accurate RGB colors for black source images when you add "color-interpolation-filters"="sRGB" to the feColorMatrix.
– John SmithApr 16 '19 at 7:34