diff --git a/src/types/index.ts b/src/types/index.ts index c68bb40..70b6e77 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -54,6 +54,7 @@ export interface ColorAdjustments { colorChromeBlue: 'none' | 'weak' | 'strong'; // Chrome effect (Blue) dynamicRange: 'auto' | 100 | 200 | 400; // Fuji-style DR tone curve (auto/100 = no extra curve) sharpening: number; // 0 to 10 (output screen sharpen strength) + hdf?: number; // 0 to 10 (Highlight Diffusion Filter: glow on the highlights only) exposureCompensation: number; // -3 to +3 EV. Camera: AE bias (hardware). Library: 2^EV matrix gain. } diff --git a/src/utils/exportEngine.ts b/src/utils/exportEngine.ts index e23bda9..a4cc888 100644 --- a/src/utils/exportEngine.ts +++ b/src/utils/exportEngine.ts @@ -300,6 +300,55 @@ export async function processAndExportPhoto( canvas.drawImage(skImage, 0, 0, paint); } + // 5b. Highlight Diffusion Filter (HDF): bloom on the bright areas only — + // the software twin of the lens' built-in diffusion filter, Photoshopped + // exactly as described: extract the highlights with a hard contrast curve, + // Gaussian-blur that selection (radius 15..50px), Screen it back over the + // photo at reduced opacity. Screen against a near-black pixel is a no-op, so + // shadows stay where they were while lit areas spread their light. + // One 0..10 knob drives both the blur radius and the opacity, and the radius + // is a fraction of the width so any source size blooms alike. + // ponytail: split into radius/amount knobs only if the 1D ramp reads coarse. + if ((adjustments.hdf ?? 0) > 0) { + const strength = (adjustments.hdf ?? 0) / 10; + const sigma = width * (0.004 + 0.015 * strength); // ~10..50px at 2560 wide + // Highlight extraction: out = 2.5*in - 1.5 -> black below 0.6, speculars + // saturate white. Composed on TOP of the recipe matrix so a monochrome look + // glows white instead of leaking the original colours back through. + const hiMatrix = [ + 2.5, 0, 0, 0, -1.5, + 0, 2.5, 0, 0, -1.5, + 0, 0, 2.5, 0, -1.5, + 0, 0, 0, 1, 0, + ]; + // The order is load-bearing and NOT the paint's: a paint color filter runs + // after the paint's image filter, i.e. the blur would smear the recipe + // matrix into the selection instead of pulling the highlights out first — + // measured as a flat +6/255 haze in the shadows with no glow on the + // speculars. Chain both as image filters instead (inner runs first). + const selection = Skia.ImageFilter.MakeColorFilter( + Skia.ColorFilter.MakeCompose(Skia.ColorFilter.MakeMatrix(hiMatrix), colorFilter), + null + ); + const glowPaint = Skia.Paint(); + glowPaint.setBlendMode(BlendMode.Screen); + glowPaint.setAlphaf(0.15 + 0.35 * strength); + glowPaint.setImageFilter( + Skia.ImageFilter.MakeCompose( + Skia.ImageFilter.MakeBlur(sigma, sigma, TileMode.Clamp, null), + selection + ) + ); + // Same source the base draw used, so the glow sits on the processed pixels. + if (paintShader) { + glowPaint.setShader(paintShader); + canvas.drawRect(Skia.XYWHRect(0, 0, width, height), glowPaint); + } else { + canvas.drawImage(skImage, 0, 0, glowPaint); + } + } + + // 6. Add Monochrome Grain Overlay if (adjustments.grain > 0) { const grainOpacity = adjustments.grain / 20; // up to 0.5 opacity diff --git a/src/utils/paramDefs.ts b/src/utils/paramDefs.ts index 69309b0..398a32e 100644 --- a/src/utils/paramDefs.ts +++ b/src/utils/paramDefs.ts @@ -161,5 +161,16 @@ export const PARAM_DEFS: { get: (a) => a.grain, set: (v) => ({ grain: v }), }, + { + key: 'hdf', + label: 'HDF', + min: 0, + max: 10, + step: 1, + defaultValue: 0, + display: String, + get: (a) => a.hdf ?? 0, + set: (v) => ({ hdf: v }), + }, ], };