web: add the B&W HIGH CONTRAST film sim to the PRESETS rail

The chip lands after ACRIPES and is a mono stock of its own, so it gets its own
baseFilter ('mono-high-contrast') rather than borrowing Acros': the PHOTO STYLE
chips are keyed by baseFilter, and the two greys must sit side by side.

The look is the B&W MIX plus a push at both ends. The mix rides the matrix — a
non-BT.709 row set (0.38/0.56/0.06, identical rows, sum 1.00) so a red roof
reads bright, a blue sky deep, and the separation is contrast before any curve.
The push rides FILM_TONE (shadow -0.32, highlight +0.26) so the ends move
without touching the midtones, and the midtone slope is SIM_CONTRAST_BIAS (4
contrast units) next to the sim's own exposure bias. The knobs stay at neutral:
a sim is colour and tone only.

Both B&W stocks being mono is now asked once, through isMonochromeBase, so the
colour-only stages (saturation, white balance, R/B fine-tune, chrome, hue
mixer) and the MONO strip label can never half-apply to one of them.
This commit is contained in:
2026-09-22 17:56:00 +07:00
parent 80512d11b5
commit c403dd04c8
5 changed files with 70 additions and 10 deletions
+10 -2
View File
@@ -1,5 +1,5 @@
import { BaseFilter, ColorAdjustments } from '../types';
import { HSL_BANDS, hslBandGaps } from './colorUtils';
import { HSL_BANDS, hslBandGaps, isMonochromeBase } from './colorUtils';
// Tone-domain adjustments (Fuji-style Dynamic Range + Highlight/Shadow).
// SkSL runtime effect over a child image shader.
@@ -331,6 +331,14 @@ const FILM_TONE: Partial<Record<BaseFilter, Partial<ToneUniforms>>> = {
// Gains are TONE_SKSL's own (sh * 0.34, hl * 0.22), so -0.12 puts the toe at
// ~5% and -0.05 trims the top ~1%.
monochrome: { sh: -0.12, hl: -0.05 },
// B&W HIGH CONTRAST. Acros' ramp with both ends pushed hard: a deeper toe
// (-0.32 against Acros' -0.12) so the darks reach true black, and a shoulder
// that LIFTS instead of rolling (-0.05 → +0.26), which is the whites step of
// the brief. Midtones see neither mask, so the long smooth stretch between
// the two ends survives — that is what keeps a hard push off the posterised
// look, and the strength the stock needs on the greys is its matrix slope
// (SIM_CONTRAST_BIAS in colorUtils), not another mask here.
'mono-high-contrast': { sh: -0.32, hl: 0.26 },
};
export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter): ToneUniforms {
@@ -347,7 +355,7 @@ export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter):
// Color Chrome depth per stop of the UI's none/weak/strong. A chrome set is a
// monochrome look, so both are forced off there: the effect is colour-only
// (the preview/export matrix skips them for monochrome for the same reason).
const colour = baseFilter !== 'monochrome';
const colour = !isMonochromeBase(baseFilter);
const chromeDepth = (v: ColorAdjustments['colorChrome'] | undefined) =>
!colour || v === 'none' || v == null ? 0 : v === 'strong' ? 0.9 : 0.45;
const blueDepth = (v: ColorAdjustments['colorChromeBlue'] | undefined) =>