React Component allowing color substitutions to be defined on an image, similar to how Chroma Key (or "Green Screen") works on video.
npm i react-chromakeyed-image
In your React app:
import ReactChromakeyedImage from 'react-chromakeyed-image';
...
<h3>Original</h3>
<img src="/static/240px-face.png" />
<h3>Chromakeyed</h3>
<ReactChromakeyedImage
src="/static/240px-face.png"
findColor="#fede58"
replaceColor="#FF0000"
/>
...
Notes:
- All instances of color
findColor
will be replaced withreplaceColor
. - Note that you can use
#rrggbb
,#rrggbbaa
,#rgb
or#rgba
forms of specifying a color. - If you omit the Alpha channel, it will be assumed to be
0xFF
.
You've probably observed some "fringes" or artifacts in the above transformed image. Unless you have very tight control over your source images (e.g. they are machine-generated), you'll need to add the tolerance
prop, which specifies a plus-or-minus range to be applied to each r
, g
, b
, and a
value in the findColor
.
import ReactChromakeyedImage from 'react-chromakeyed-image';
...
<h3>Original</h3>
<img src="/static/240px-face.png" />
<h3>Chromakeyed [Tolerant]</h3>
<ReactChromakeyedImage
src="/static/240px-face.png"
findColor="#fede58"
replaceColor="#FF0000"
tolerance={10}
/>
...
If you need to transform more than one color, supply a colorReplacementMap
prop, using #rrggbb[aa]
-style colors as before:
import ReactChromakeyedImage from 'react-chromakeyed-image';
...
<h3>Original</h3>
<img src="/static/240px-face.png" />
<h3>Chromakeyed [Mapped]</h3>
<ReactChromakeyedImage
src="/static/240px-face.png"
colorReplacementMap={{ "#fede58": "#00FF00", "#871945": "#00f"}}
/>
...
To avoid the fringing effects visible in the above image, you can add the tolerance
prop when using a colorReplacementMap
too:
import ReactChromakeyedImage from 'react-chromakeyed-image';
...
<h3>Original</h3>
<img src="/static/240px-face.png" />
<h3>Chromakeyed [Mapped, Tolerant]</h3>
<ReactChromakeyedImage
src="/static/240px-face.png"
colorReplacementMap={{ "#fede58": "#00FF00", "#871945": "#00f"}}
tolerance={20}
/>
...
Sometimes, what you need to do can't be expressed with a static map. For those times, you can supply a function as the replacementFunction
prop. The function takes 3 arguments, as per the following TypeScript declarations:
export type RGBAPixel = {
r: number;
b: number;
g: number;
a: number;
}
export type PixelReplacementFunction = (pixel: RGBAPixel, x:number, y:number) => RGBAPixel;
The first argument is the original {r, g, b, a}
value of the pixel. Then come the x
and y
co-ordinates of that pixel. The function should always return a pixel in the form {r, g, b, a}
, even if no change was made to it.
This allows you to apply different replacements depending on the co-ordinates within the image, as in the following example, which only makes changes to a small horizontal band of pixels, leaving all others unchanged:
import ReactChromakeyedImage from 'react-chromakeyed-image';
...
<h3>Original</h3>
<img src="/static/240px-face.png" />
<h3>Chromakeyed [Custom function]</h3>
<ReactChromakeyedImage
src="/static/240px-face.png"
replacementFunction={ ( { r,g,b,a }, x, y ) => {
if ( y > 50 && y < 120) {
return { r: 0x30, g: 0x30, b: 0x30, a};
}
return { r, g, b, a };
}}
/>
...
Simply replacing each pixel might be too crude - you may be looking for a more "layered" effect where the background image can still be made out through your foreground replacement, using Alpha transparency. In this case, supply a blendMode
prop with a value from the BlendMode
enumeration:
export enum BlendMode {
OPAQUE_FOREGROUND, // This is the default
ALPHA_BLENDING,
ALPHA_RETAIN_BG_TRANSPARENCY
};
Using the Alpha blending algorithm, the amount of a
returned in your map/function for a given pixel will control how much of the background shows through - 0xFF
would be fully opaque (like the default behaviour), 0x80
would be half-visible, etc:
import ReactChromakeyedImage from 'react-chromakeyed-image';
...
<h3>Original</h3>
<img src="/static/240px-face.png" />
<h3>Chromakeyed [Alpha Blending]</h3>
<ReactChromakeyedImage blendMode={BlendMode.ALPHA_BLENDING} src="/static/240px-face.png" replacementFunction={({r,g,b,a},x, y) => {
if ( y > 50 && y < 120) {
return { r: 0x30, g: 0x30, b: 0x30, a };
}
return { r, g, b, a};
}}
/>
...
Alpha blending is good but if your source background image already has fully-transparent pixels (as in our sample face image), they are probably there for good reason. Note what happened to our image when we Alpha-blended it above; we lost the full transparency around the edges. In cases like these, use BlendMode.ALPHA_RETAIN_BG_TRANSPARENCY
- this checks to see if the Alpha channel of the background pixel is 0x00
(i.e. fully transparent) and if so, doesn't bother applying any transformation:
import ReactChromakeyedImage from 'react-chromakeyed-image';
...
<h3>Original</h3>
<img src="/static/240px-face.png" />
<h3>Chromakeyed [Alpha Blending (retained BG transparency)]</h3>
<ReactChromakeyedImage blendMode={BlendMode.ALPHA_RETAIN_BG_TRANSPARENCY} src="/static/240px-face.png" replacementFunction={({r,g,b,a},x, y) => {
if ( y > 50 && y < 120) {
return { r: 0x30, g: 0x30, b: 0x30, a };
}
return { r, g, b, a};
}}
/>
...
Any props you give to ReactChromakeyedImage
will be spread onto the underlying HTML canvas
, so you can control the overall appearance of the image however you like; e.g.:
<h3>Original</h3>
<img src="/static/240px-face.png" />
<h3>Chromakeyed (and styled)</h3>
<ReactChromakeyedImage
style={{width: '100px',
height: '100px',
border: '3px solid black',
borderRadius: '8px' }}
src="/static/240px-face.png"
findColor="#fede58"
replaceColor="#FF0000"
/>
Check out PixelUtils
and ColorStringUtils
for functions that might be useful when writing your own custom pixel replacement functions.