65d0850317
- 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.
89 lines
3.8 KiB
TypeScript
89 lines
3.8 KiB
TypeScript
import { ColorAdjustments } from '../types';
|
||
|
||
// Tone-domain adjustments (Fuji-style Dynamic Range + Highlight/Shadow).
|
||
// SkSL runtime effect over a child image shader, driven by luma so hue is
|
||
// preserved (single multiplicative factor per pixel).
|
||
//
|
||
// Soft-knee masks keep the knobs independent, like camera tone curves:
|
||
// hl affects ONLY the bright end (knee 0.80..1.00),
|
||
// sh affects ONLY the dark end (knee 0.00..0.30),
|
||
// leaving the whole 0.30..0.80 mid/upper band untouched. Earlier knees
|
||
// (0.40..0.95 / 0.05..0.55, then 0.55..1.0 / 0.0..0.45, then 0.70..1.0) still
|
||
// reached mid/upper grays, so HL read as a global gray/brightness change
|
||
// instead of isolating the bright end — on-device testing confirmed it.
|
||
//
|
||
// HL is multiplicative on BOTH signs so +/− act on the same region:
|
||
// + scales 0.85+ up toward white (natural clip at 1.0), − scales them down.
|
||
// A + additive lift using (1-t) dies at t→1.0 and never touched the specular
|
||
// end — that asymmetry made +HL look like a mid-tone push, not a highlight
|
||
// change. (SH keeps its original form; additive + worked there.)
|
||
//
|
||
// dr - DR strength 0..1: lifts shadows slightly and rolls highlights
|
||
// (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);
|
||
float hlMask = smoothstep(0.80, 1.0, t);
|
||
float shMask = 1.0 - smoothstep(0.0, 0.30, t);
|
||
// NOTE: never name a local 'out' — it is a reserved SkSL qualifier.
|
||
float o = t;
|
||
// Highlight: bright end only, multiplicative on both signs (symmetric region).
|
||
if (hl >= 0.0) {
|
||
o *= 1.0 + hl * 0.30 * hlMask;
|
||
} else {
|
||
o *= 1.0 + hl * 0.45 * hlMask;
|
||
}
|
||
// Shadow: dark end only.
|
||
if (sh >= 0.0) {
|
||
o += sh * 0.80 * shMask * (1.0 - t);
|
||
} else {
|
||
o *= 1.0 + sh * 0.80 * shMask;
|
||
}
|
||
// Dynamic range: gentle shadow lift + highlight roll (protect brights).
|
||
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);
|
||
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);
|
||
}
|
||
`;
|
||
|
||
export interface ToneUniforms {
|
||
// All zero → no tone adjustment needed (caller can skip the shader pass).
|
||
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 {
|
||
const drRaw = adj.dynamicRange ?? 'auto';
|
||
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));
|
||
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 || u.vib !== 0;
|
||
}
|