Add HDF (Highlight Diffusion Filter) to the FX tab
The built-in lens filter, simulated the way it is done in Photoshop: pull the highlights out with a hard contrast curve (out = 2.5*in - 1.5, black below 0.6), Gaussian-blur that selection, then Screen it back over the photo at reduced opacity. Screen against a near-black pixel is a no-op, so areas with no highlight within the blur radius come back byte-identical while lit areas spread their light. One 0..10 knob drives both the radius (4%..1.9% of the width, ~10..50px at 2560 wide) and the opacity (0.15..0.5), matching the recipe's 15-50px radius range. The extraction composes on top of the recipe colour matrix so a monochrome look glows white instead of leaking the original colours back. Runs in the Skia export engine only; the P5 native path is not wired into the app (dev parity probe only), so it is left alone for now.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user