How to Recolour the Muscle Highlight to Your Brand Colour
Recolour the orange muscle highlight in RepDB exercise images, animations and heatmap layers to your brand colour, in your own code.
Every RepDB classic-style image and animation shows the working muscles in orange on a neutral grey figure (the flat illustration style has no muscle highlight). If your app’s accent colour is wine red or green, orange looks like it belongs to somebody else’s product.
You can change it, and you do not need a custom delivery. Recolour the highlight in your own code. That is a colour adjustment, which the Standard and Starter licenses allow.
Same animation, orange to green
The animation is the running clip from the free preview bundle; the recoloured clip has the same 49 frames and timing. Left to right: the original, a whole-image CSS filter (live, applied by your browser right now), and the selective recolour to #00B248 (a green a customer asked me for), made with the build-time script below.



#00B248The filter in the middle is the recipe that works for heatmap layers. On a full image it also lifts the grey figure by about 10%. With green that is easy to miss, but it is not the highlight alone, and slightly tinted greys can shift hue (the rowing-machine still in the same preview turns brownish). With a darker brand colour the side effect is obvious: the wine-red recipe (brightness(0.64)) makes the grey figure about a third darker. The right-hand card changes the highlight only.
In short
- Muscle heatmap layers (Standard kit): one colour filter computed from your hex code, no image processing.
- Classic images and animations: run a small script once per brand colour. It recolours only the orange pixels and leaves the grey figure, the equipment and the transparency alone. A whole-image filter changes the figure too, see the note under the demo.
- One run per colour. You generate the variants from the files you already have; nothing is needed from us. A 96-frame, 960 px clip takes about 40 seconds.
- Allowed by the licence, as long as it stays plain colour adjustment (no AI restyling).
The quickest way: hand the technical part to your coding agent. Copy this, fill in your colour, and give it to your developer or to a coding agent:
Recolour the orange muscle highlight in my RepDB classic images, animated
WebP files and muscle diagrams to #RRGGBB. Skip the flat images: they have
no muscle highlight, and their skin tones would be recoloured. Follow
https://repdb.co/blog/recolor-exercise-muscle-highlight-brand-color/ :
use the build-time Python script from "At build time, for animations" for
images and animations, write the results to a separate folder and keep the
originals. For muscle heatmap layers use the colour-matrix filter from
"Route 1". Do not use any AI image model for this.
Technical details
Everything from here on is for the developer, or the agent doing the work. All of it was run on real files from the Standard bundle. The last section lists what I did not check.
Route 1: heatmap layers, one colour filter
The Standard bundle’s muscle heatmap kit is a neutral body plus one transparent overlay per muscle. Each overlay contains only the highlight, so a filter on the layer cannot touch anything else.
The filter that works for any colour is a colour matrix. It does what step 2 of Route 2 does: it takes each pixel’s brightness and puts your colour at that brightness, so the muscle shading stays. The numbers come straight from your hex code, nothing to fit. Put the filter in the page once:
<svg width="0" height="0" style="position: absolute" aria-hidden="true">
<filter id="brand-highlight" color-interpolation-filters="sRGB">
<!-- values for #00B248; highlightMatrix() below computes them for any colour -->
<feColorMatrix type="matrix"
values="0 0 0 0 0 0.3533 1.1887 0.12 0 0 0.1429 0.4808 0.0485 0 0 0 0 0 1 0" />
</filter>
</svg>
.muscle-layer {
filter: url(#brand-highlight);
}
To set the colour from code, for example from your theme:
// brightness of the stock orange in the Standard kit's layers
// (the female add-on's layers are lighter: use 0.50 there)
const KIT_LUMA = 0.42;
function highlightMatrix(hex, refLuma = KIT_LUMA) {
const k = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255 / refLuma);
const row = (x) => [x * 0.2126, x * 0.7152, x * 0.0722, 0, 0].map((v) => +v.toFixed(4)).join(' ');
return [...k.map(row), '0 0 0 1 0'].join(' ');
}
document.querySelector('#brand-highlight feColorMatrix')
.setAttribute('values', highlightMatrix('#7E3A46'));
Keep color-interpolation-filters="sRGB": without it the browser applies the matrix in linear light and the colour comes out different. The stock layers are not all equally bright, and they stay that way after the filter: a lighter muscle stays lighter. Opacity per layer still works as the intensity control, as in the kit’s own README.
Without an inline SVG. The showcase page recolours the same layers with plain CSS filter chains instead. They work, but each one is fitted by hand for one colour, so for your own colour you search hue, then saturation, then brightness by eye:
.muscle-layer.wine {
/* stock orange → wine red, about #7E3A46 */
filter: hue-rotate(-28deg) saturate(0.75) brightness(0.64);
}
.muscle-layer.green {
/* stock orange → green, about #00B248 */
filter: hue-rotate(130deg) saturate(1.4) brightness(1.1);
}
Route 2: images and animation frames need a selective recolour
An exercise still or an animation frame is a grey figure with an orange highlight. A whole-image filter treats both the same, so any recipe that makes the highlight darker (like wine red) also makes the figure darker, and any recipe that brightens it washes the figure out.
The fix is to change only the pixels that are orange and to keep their light and shade, so the fibre texture survives:
- Select by hue and saturation. The figure, equipment and shorts are grey (saturation close to zero). The highlight is strongly saturated and sits around hue 20°. A smooth weight between 0 and 1 handles the anti-aliased edge pixels.
- Keep the shading. Measure the mean luma of the orange in this file first (over all frames of an animation). For selected pixels, take the pixel’s luma relative to that mean and apply the factor to your brand colour. A darker fibre stays darker, a lighter one stays lighter, and on average the highlight lands on your colour.
- Split the outline. A pixel on the border between the highlight and the grey figure is a mix of both. Without care, the recolour leaves a bright rim there. Estimate how much orange the pixel contains from its saturation, recolour that part, and keep the grey part.
- Leave alpha alone. Transparency is never touched.
I checked the assumption in step 1 on a sample of 60 classic stills and 25 animations from the Standard bundle: about 99% of strongly saturated pixels fall in the orange range (hue 10–30°). Pixels outside the weight’s range come out byte-identical to the input.
In the browser, for stills
// data: Uint8ClampedArray RGBA (ImageData.data); target: [r, g, b] 0-255
const STOCK = [0.846, 0.380, 0.158]; // mean stock orange
const smooth = (a, b, x) => {
const t = Math.min(Math.max((x - a) / (b - a), 0), 1);
return t * t * (3 - 2 * t);
};
const luma = (r, g, b) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
// how strongly a pixel belongs to the orange highlight (0-1), and its saturation
function select(r, g, b) {
const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = Math.max(mx - mn, 1e-6);
const sat = mx > 0 ? (mx - mn) / mx : 0;
if (sat < 0.03) return [0, sat]; // grey figure: untouched
let hue = (mx === r ? ((g - b) / d) % 6 : mx === g ? (b - r) / d + 2 : (r - g) / d + 4) * 60;
if (hue < 0) hue += 360;
let dist = Math.abs(hue - 20);
dist = Math.min(dist, 360 - dist);
return [smooth(0.03, 0.18, sat) * (1 - smooth(30, 50, dist)), sat];
}
// mean luma of the pixels fully inside the highlight: the reference for the shading
function highlightLuma(data) {
let total = 0, n = 0;
for (let i = 0; i < data.length; i += 4) {
const r = data[i] / 255, g = data[i + 1] / 255, b = data[i + 2] / 255;
const [w, sat] = select(r, g, b);
if (w === 1 && sat / 0.78 >= 0.95 && data[i + 3] / 255 > 0.5) {
total += luma(r, g, b);
n++;
}
}
return n ? Math.floor((total / n) * 1000 + 0.5) / 1000 : 0.46;
}
function recolorHighlight(data, target, refLuma = highlightLuma(data)) {
const T = target.map((v) => v / 255);
for (let i = 0; i < data.length; i += 4) {
const rgb = [data[i] / 255, data[i + 1] / 255, data[i + 2] / 255];
const [w, sat] = select(...rgb);
if (w === 0) continue;
let k = luma(...rgb) / refLuma; // shading of this pixel
if (k > 1) k = 1 + 0.5 * (k - 1); // soften the brightest fibres
// outline pixels mix orange with the grey next to it: split them
const c = Math.min(Math.max(sat / 0.78, 0), 1); // how much orange is in the pixel
const t = smooth(0.7, 0.95, c);
for (let j = 0; j < 3; j++) {
const inside = Math.min(T[j] * k, 1);
const grey = Math.min(Math.max((rgb[j] - c * STOCK[j]) / Math.max(1 - c, 1e-3), 0), 1);
const edge = c * T[j] + (1 - c) * grey;
const out = edge * (1 - t) + inside * t;
data[i + j] = ((rgb[j] * (1 - w) + out * w) * 255 + 0.5) | 0;
}
}
}
const img = document.querySelector('#exercise'); // a loaded, same-origin still
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.drawImage(img, 0, 0);
const frame = ctx.getImageData(0, 0, canvas.width, canvas.height);
recolorHighlight(frame.data, [0x7e, 0x3a, 0x46]);
ctx.putImageData(frame, 0, 0);
highlightLuma measures the reference from the image itself: the mean luma of the pixels fully inside the highlight. The stock orange is not equally bright in every exercise (from 0.35 to 0.54 on 60 classic stills from the Standard bundle, 0.45 on average), so a fixed reference would leave the result anywhere from about 25% darker to 15% lighter than your colour. With the measured reference, a #7E3A46 target averaged between #783844 and #7D3B47 on six classic stills and #7B3A46 on the running clip; a fixed 0.46 gave #6B343F to #843F4B on the same files. 0.46 stays only as the fallback for an image with no highlight.
Recolour once and cache the result: do not run this per frame on a list of cards.
At build time, for animations
For a looping animation, recolour the frames once in your asset pipeline and ship the result as an ordinary animated WebP. This script does that with Pillow and NumPy and keeps the frame timing and loop count:
"""python recolor_webp.py in.webp out.webp "#7E3A46" """
import sys
import numpy as np
from PIL import Image, ImageSequence
LUMA = np.array([0.2126, 0.7152, 0.0722])
STOCK = np.array([0.846, 0.380, 0.158]) # mean stock orange
def smoothstep(e0, e1, x):
t = np.clip((x - e0) / (e1 - e0), 0, 1)
return t * t * (3 - 2 * t)
def select(rgb):
"""How strongly each pixel belongs to the orange highlight (0-1), and its saturation."""
mx, mn = rgb.max(-1), rgb.min(-1)
d = np.maximum(mx - mn, 1e-6)
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
hue = np.where(mx == r, ((g - b) / d) % 6,
np.where(mx == g, (b - r) / d + 2, (r - g) / d + 4)) * 60
sat = (mx - mn) / np.maximum(mx, 1e-6)
dist = np.abs(hue - 20)
dist = np.minimum(dist, 360 - dist)
return smoothstep(0.03, 0.18, sat) * (1 - smoothstep(30, 50, dist)), sat
def highlight_luma(frames):
"""Mean luma of the pixels fully inside the highlight, over all frames."""
total, n = 0.0, 0
for rgba in frames:
w, sat = select(rgba[..., :3])
core = (w == 1) & (sat / 0.78 >= 0.95) & (rgba[..., 3] > 0.5)
total += (rgba[..., :3] * LUMA).sum(-1)[core].sum()
n += int(core.sum())
return int(total / n * 1000 + 0.5) / 1000 if n else 0.46
def recolor(rgba, target, ref_luma):
rgb = rgba[..., :3]
w, sat = select(rgb)
w = w[..., None]
k = (rgb * LUMA).sum(-1, keepdims=True) / ref_luma # shading of this pixel
k = np.where(k > 1, 1 + 0.5 * (k - 1), k) # soften the brightest fibres
inside = np.clip(np.array(target) * k, 0, 1)
# outline pixels are a mix of orange and the grey next to it: split them
c = np.clip(sat / 0.78, 0, 1)[..., None] # how much orange is in the pixel
grey = np.clip((rgb - c * STOCK) / np.maximum(1 - c, 1e-3), 0, 1)
edge = c * np.array(target) + (1 - c) * grey
t = smoothstep(0.7, 0.95, c)
new = edge * (1 - t) + inside * t
rgb = rgb * (1 - w) + new * w
return np.concatenate([rgb, rgba[..., 3:]], -1)
src, dst, hex_color = sys.argv[1:4]
target = [int(hex_color[i:i + 2], 16) / 255 for i in (1, 3, 5)]
im = Image.open(src)
rgba = lambda fr: np.asarray(fr.convert("RGBA")).astype(float) / 255
ref_luma = highlight_luma(rgba(fr) for fr in ImageSequence.Iterator(im))
frames, durations = [], []
for fr in ImageSequence.Iterator(im):
frames.append(Image.fromarray((recolor(rgba(fr), target, ref_luma) * 255 + 0.5).astype(np.uint8)))
durations.append(fr.info.get("duration", 50))
frames[0].save(dst, save_all=True, append_images=frames[1:], duration=durations,
loop=im.info.get("loop", 0), quality=90, method=4, exact=True)
On a real 960 px, 96-frame animation from the Standard bundle this took about 40 seconds on my machine; the script reads the frames twice, once to measure the highlight and once to recolour. The output has the same 96 frames, the same 50 ms frame timing and the same infinite loop, and the file is about the same size (4.4 MB against 4.6 MB). The exact=True flag keeps the colour values of fully transparent pixels from being rewritten. The same script works on stills.
The JavaScript and Python versions are the same algorithm. I ran both on two 1024 px classic stills and on a frame of the running clip, with two target colours, and compared the output bytes: they are identical. One difference in use: for an animation the Python script measures the reference over all frames, so a single frame recoloured in the browser can come out very slightly different from the same frame in the recoloured clip.
If you ship several brand colours, run the script once per colour and keep the results next to the originals, the same way you would keep a dark-mode variant. That fits the pattern from Fitness App Themes with Transparent Animations: the theme decides the palette, the pipeline produces the files.
Native apps
The approach carries over to iOS and Android, but I have not built or run the native versions, so treat this as direction, not tested code:
- Heatmap layers: the 20 numbers from
highlightMatrix()are the same 4×5 matrix that Android’sColorMatrixtakes, and Core Image’sCIColorMatrixtakes the same rows as its R, G, B and A vectors. Core Image works in linear light by default, like the SVG filter withoutcolor-interpolation-filters="sRGB", so give it an sRGB working colour space or the shade will differ. - Images and animations: the build-time script above works for every platform because the app just ships the recoloured WebP files. This is the safest native route.
Licence
Standard and Stills license v1.2 lists what you can do with the images: “Resize, crop, compress, or colour-adjust the images for use in your products”. Recolouring the muscle highlight with the filter or scripts above is that kind of change: deterministic pixel arithmetic on files you already have. What stays prohibited is AI-based restyling (image-to-image, style transfer, fine-tuning on the images). Two scope limits to keep in mind: Standard and Stills cover products under your own brand, so apps delivered under a client’s brand need the Enterprise terms, and the muscle heatmap kit ships with Standard only.
What I checked and what I did not
Checked
- The colour-matrix filter: in Chromium, on the showcase’s heatmap layers, for green and wine red.
- The two CSS filter recipes: numerically on the 12 demo layers with the filter-matrix definitions from the CSS spec, and in Chromium on a real layer.
- The selective recolour on real frames from the Standard bundle: stills and a 96-frame animation, two target colours, grey pixels unchanged, alpha unchanged, timing and loop preserved. The JavaScript version also ran in Chromium on a real frame, with no orange pixels left afterwards.
- The JavaScript and Python versions produce identical output.
Not checked
- Live recolour of an animated WebP on a canvas. In my Chromium test, drawing an animated
<img>onto a canvas kept returning the same frame, so I could not confirm a per-frame runtime recolour. Recolour animations at build time, or decode the frames yourself. - The colour-matrix filter in Firefox and Safari. SVG filters on HTML elements are a long-standing feature there too, but I only looked at Chromium.
- iOS and Android code, as noted above.
- Fine edges. Outline pixels, where the orange meets the grey figure, are split into their orange and grey parts and only the orange part is recoloured. At 5× zoom a thin dark line can remain where the highlight borders the dark shorts, and a few stray pixels are partly recoloured. At normal size I do not see it, but check your own colour on your own surface.
- Oranges that are not the highlight. Anything strongly orange in a frame is recoloured, because the selection is by hue. In the classic frames I sampled the figure, equipment and shorts are grey. The flat style is the case to avoid: it has no muscle highlight, and its skin tones fall in the same hue range, so the script would turn the skin green. Check this too if you add your own artwork next to the RepDB frames.
- Every exercise. I tested a sample, not all of the catalogue.
FAQ
Can I change the orange muscle highlight to my brand colour?
Yes. Recolour it in your own code. Heatmap layers take a colour filter computed from your hex code; images and animation frames need a selective recolour of the orange pixels only. No custom delivery is needed.
Does the licence allow recolouring?
Yes. Standard and Stills license v1.2 allows you to “resize, crop, compress, or colour-adjust” the images for use in your products. AI-based restyling is not allowed.
Why not just put a CSS filter on the image?
A filter acts on every pixel. A recipe that turns the highlight dark also darkens the grey figure and the equipment. Use a filter on heatmap layers (which contain only the highlight) and a selective recolour on images and animations.
Can I recolour the animations at runtime?
For stills, yes, on a canvas. For animated WebP I only verified a build-time recolour, which keeps frame count, timing and loop. Plan on recolouring animations once in your pipeline.
Do I need a separate set of files per brand colour?
Not from RepDB. You generate the variants from the files you already have, one run per colour.