RepDB
← All posts

How to Recolour the Muscle Highlight to Your Brand Colour

October 13, 2026 · 18 min read · by Sergei Argutin

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.

Running animation with the stock orange highlight on the legs and glutes
Original — stock orange
The same exercise with a whole-image CSS filter: the highlight is green, but the figure and shorts shift as well
Whole-image filter — the whole figure brightens
The same exercise with only the highlight recoloured to green #00B248; the figure is unchanged
Selective — only the highlight, #00B248

The 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

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

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

Not checked

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.

Want to see more from RepDB?

Add RepDB as a Preferred Source to find more of our exercise data in Google.