FX vignetting, LIGHT vibrance/saturation, reticle unlock

- FX: new VIGNETTING 0-10 control (chip + slider). Darkens the four
  corners of the photo with a radial gradient whose radius is the photo
  diagonal/2, so the falloff stays circular on any aspect. Applied in
  preview (camera overlay, polaroid/wall window, plain library) and in
  exportEngine pass 6b, always on the picture, never on the mat.
- LIGHT: new VIBRANCE -10..+10 (chroma-masked saturation in the tone
  shader: vivid pixels move least). Renamed the existing saturation
  control chip from COLOR to SATURATION - same param, clearer label.
- Fix: <Shader> takes a RuntimeEffect, not a gradient SkShader; the
  preview vignette threw "Object is not a HostObject" when it got one.
  Use the declarative <RadialGradient> child on the Rect instead.
- Preview: drop the "AE/AF LOCKED - TAP TO UNLOCK" banner; the lock
  reticle is now itself the unlock touch target.
This commit is contained in:
2026-09-10 10:22:55 +07:00
parent d7a8e0681e
commit 65d0850317
5 changed files with 121 additions and 26 deletions
+17 -3
View File
@@ -22,11 +22,16 @@ import { ColorAdjustments } from '../types';
// (Fuji extended DR); 0/auto/DR100 = no extra curve.
// hl - highlight -1..1: + lifts toward white, - rolls the bright side down.
// sh - shadow -1..1: + lifts the dark side, - deepens it.
// vib - vibrance -1..1: chroma-masked saturation. It rides along in this shader
// (rather than the colour matrix) because it needs per-pixel chroma:
// already-vivid pixels move least, so skins/skies deepen without the neon
// clip a plain Saturation boost causes. All-zero still = no pass.
export const TONE_SKSL = `
uniform shader src;
uniform float dr;
uniform float hl;
uniform float sh;
uniform float vib;
vec4 main(vec2 xy) {
vec4 c = src.eval(xy);
float t = clamp(dot(c.rgb, vec3(0.2126, 0.7152, 0.0722)), 0.0, 1.0);
@@ -50,7 +55,14 @@ vec4 main(vec2 xy) {
o += dr * 0.12 * shMask * (1.0 - t);
o -= dr * 0.18 * hlMask * t;
float f = clamp(o / max(t, 0.0004), 0.0, 4.0);
return vec4(clamp(c.rgb * f, 0.0, 1.0), c.a);
vec3 rgb = clamp(c.rgb * f, 0.0, 1.0);
// Vibrance: push the LESS-saturated pixels harder than the vivid ones.
float l2 = dot(rgb, vec3(0.2126, 0.7152, 0.0722));
float mx = max(max(rgb.r, rgb.g), rgb.b);
float mn = min(min(rgb.r, rgb.g), rgb.b);
float chroma = mx > 0.0001 ? (mx - mn) / mx : 0.0;
float kv = 1.0 + vib * 0.75 * (1.0 - chroma);
return vec4(clamp(mix(vec3(l2), rgb, kv), 0.0, 1.0), c.a);
}
`;
@@ -59,6 +71,7 @@ export interface ToneUniforms {
dr: number; // 0..1
hl: number; // -1..1 (adjustments.highlight / 10)
sh: number; // -1..1 (adjustments.shadow / 10)
vib: number; // -1..1 (adjustments.vibrance / 10)
}
export function getToneUniforms(adj: ColorAdjustments): ToneUniforms {
@@ -66,9 +79,10 @@ export function getToneUniforms(adj: ColorAdjustments): ToneUniforms {
const dr = drRaw === 'auto' || drRaw === 100 ? 0 : (drRaw - 100) / 300;
const hl = Math.max(-1, Math.min(1, (adj.highlight ?? 0) / 10));
const sh = Math.max(-1, Math.min(1, (adj.shadow ?? 0) / 10));
return { dr, hl, sh };
const vib = Math.max(-1, Math.min(1, (adj.vibrance ?? 0) / 10));
return { dr, hl, sh, vib };
}
export function toneIsActive(u: ToneUniforms): boolean {
return u.dr !== 0 || u.hl !== 0 || u.sh !== 0;
return u.dr !== 0 || u.hl !== 0 || u.sh !== 0 || u.vib !== 0;
}