Choose the reds / original
DEVELOPMENT JOURNAL / 17
Photoshot
GITHUB × AI
Building a free Photoshop alternative

Choose the reds

This Photoshot journal follows the development of a free image editor. Selective Color changes components within color families instead of rotating every color at once. We read the actual weighting and relative-mode calculations, then compare a new portrait’s clothing, skin and background.

PHOTOSHOT DEVELOPMENT JOURNAL / 17

AI-generated study photograph. Editor captures and outputs show Photoshot in use.

PhotoshotDEVELOPMENT JOURNAL / 17FOUNDATION

From the source.

A color family is not an object selection

Choosing Reds to change a red sweater does not make the editor recognize clothing. Red components in lips and skin can enter the same calculation. To restrict the adjustment to clothing, a layer mask must also define the spatial area.

Selective Color weights pixel color families and connects cyan, magenta, yellow and black controls to changes in RGB. These CMYK-named controls do not convert the document to a print CMYK profile.

Nine families use weights, not hard labels

The public code defines red, yellow, green, cyan, blue, magenta, white, neutrals and black. It finds each pixel’s highest, lowest and middle RGB values. The red weight is hi−mid when R is the highest channel, otherwise zero. The effect therefore depends on how far red sits above the other channels.

Whites use how far the lowest channel exceeds 0.5; blacks use how far the highest channel falls below 0.5. Neutrals use 1−|hi+lo−1|. These are not mutually exclusive categories. Adjustments to several families can accumulate within the same pixel.

src/adjustments.ts · d08d365 · L748–761Photoshot implementation ↗
    if (t === 'selective') {
      const hi = Math.max(r, g, b),
        lo = Math.min(r, g, b),
        mid = r + g + b - hi - lo;
      weights[0] = r === hi ? hi - mid : 0;
      weights[1] = b === lo ? mid - lo : 0;
      weights[2] = g === hi ? hi - mid : 0;
      weights[3] = r === lo ? mid - lo : 0;
      weights[4] = b === hi ? hi - mid : 0;
      weights[5] = g === lo ? mid - lo : 0;
      weights[6] = clamp((lo - 0.5) * 2);
      weights[7] = 1 - Math.abs(hi + lo - 1);
      weights[8] = clamp((0.5 - hi) * 2);
      const input = scratch;
PhotoshotDEVELOPMENT JOURNAL / 17IMPLEMENTATION

Inside the
calculation.

Relative and absolute change the amount

Each RGB output channel receives its corresponding C, M or Y control plus K. Relative mode multiplies the color component by 1−input channel and the black component by the input channel. Turning Relative off uses a multiplier of one for both. Both modes still apply family weights and clamp the output to 0–1.

Increasing magenta acts by reducing the green channel. Absolute mode does not mean every pixel changes equally: family weights remain active. This describes the quoted Photoshot implementation, not numerical equivalence with Adobe Photoshop.

A red sweater and a green leaf

On October 22, 2026, we opened a new AI portrait in production Photoshot. Only Reds were changed: C 0, M +35, Y −25, K +10. The other eight families remained at zero. Relative was on, with Normal blending, 100% opacity and no spatial mask restriction.

Original and output are the same 1536×1024 frame. The sweater shifts toward magenta, with visible changes to lips and skin as well. The cyan background and green leaf remain comparatively stable. The model is AI-generated, not an actual celebrity photograph or endorsement. The editor capture, exported PNG and editable project were retained.

PHOTOSHOT / SOURCE NOTES17

Read the code.

src/adjustments.ts · d08d365 · L766–778Photoshot implementation ↗
        let delta = 0;
        for (let k = 0; k < 9; k++) {
          const ink = v[k + 'CMY'[c]] / 100,
            black = v[k + 'K'] / 100;
          delta +=
            weights[k] *
            (ink * (v.relative ? 1 - input[c] : 1) + black * (v.relative ? input[c] : 1));
        }
        out[c] = clamp(input[c] - delta);
      }
    }
    if (t === 'lookup' && cube) {
      const cr = shaperValue(cube, 0, r),
src/adjustments.ts · d08d365 · L210–220Photoshot implementation ↗
  selective: {
    name: '선택 색상',
    description: '색상 계열별로 청록·자홍·노랑·검정 성분을 조절합니다.',
    controls: colorNames
      .flatMap((_, i) =>
        ['C', 'M', 'Y', 'K'].map((c, j) =>
          control(i + c, ['청록', '자홍', '노랑', '검정'][j], -100, 100, 0, 1, String(i)),
        ),
      )
      .concat(control('relative', '상대값', 0, 1, 1)),
  },
tests/adjustment-render.mjs · d08d365 · L127–134Photoshot implementation ↗
check('selective color targets one color family', () => {
  const a = A.makeAdjustment('selective');
  a.values['0M'] = 100;
  const p = Uint8ClampedArray.from([220, 90, 90, 255, 90, 220, 90, 255]);
  A.adjustPixels(p, a);
  assert(p[1] < 90);
  close(Array.from(p.slice(4)), [90, 220, 90, 255]);
});
PhotoshotDEVELOPMENT JOURNAL / 17IN PRACTICE

In the editor.

Choose the reds / workspace
Actual Photoshot workspace. Settings and the image are shown together.

Verification and the next development scope

The public source is pinned to commit d08d365. On October 22, 2026, adjustment-render passed all 22 test groups. Its dedicated Selective Color check increases magenta in a red pixel, verifies reduced green, and checks that a separate green pixel stays unchanged. General coverage includes alpha, adjustment masks, compositing and project compatibility.

We also inspected the production project for all four Reds values, relative=1, zero values in the other families and opacity=1. This does not certify every photographic color boundary or print workflow. Part 18 moves to the menu and feature verification matrix, clearly separating the public repository from the production implementation.

PhotoshotDEVELOPMENT JOURNAL / 17BEFORE / AFTER

The visible result.

Choose the reds / original
01 / AI-generated study image / before editing
Choose the reds / result
02 / Photoshot output / edited result

AI-generated study photograph. Editor captures and outputs show Photoshot in use.