diff --git a/App.tsx b/App.tsx index ec9780c..45ae053 100644 --- a/App.tsx +++ b/App.tsx @@ -402,6 +402,32 @@ export default function App() { // never flip the watermark on/off. }; + // The recipe exactly as the user picked it. `selectedRecipe` is NOT a safe + // copy of it: every slider move rewrites its `adjustments` in place + // (handleUpdateAdjustments), so the canonical values only survive in the + // film-sim table and in the saved list. + const pristineRecipe = (recipe: Recipe): Recipe | null => { + const sim = FILM_SIMS.find((f) => f.id === recipe.id); + if (sim) return filmSimToRecipe(sim); + return recipes.find((r) => r.id === recipe.id) || null; + }; + + // Every edit belongs to the mode and the photo it was made on, so it never + // crosses into the next session: the sliders go back to the recipe's own + // values, a frame picked for that still is dropped, and the custom mark — + // placed against that very photo — is switched back off. + const resetEditsToRecipe = () => { + const base = selectedRecipe ? pristineRecipe(selectedRecipe) : null; + if (base) { + applyRecipe(base); + } else { + setAdjustments({ ...DEFAULT_ADJUSTMENTS }); + setSelectedFrame('none'); + } + setCustomWm({ ...DEFAULT_CUSTOM_WM }); + setCustomWmApplied({ ...DEFAULT_CUSTOM_WM }); + }; + const handleUpdateAdjustments = (updates: Partial) => { // Storm guard, generic across every param: same-value writes (slider // micro-jitter / repeated pointer-move events) still build a fresh @@ -595,6 +621,10 @@ export default function App() { setLibraryImageUri(asset.uri); setPhotoRotation(0); setMode('library'); + // Picking from the camera is the other half of that switch (the camera + // button is the way back), so the camera's edits must not land on the + // photo: the picked still starts at the recipe's own values too. + if (mode !== 'library') resetEditsToRecipe(); // Clear old GPS — will be replaced if the photo (or device) has location data setGpsInfo(null); @@ -854,17 +884,11 @@ export default function App() { // placement layer left eating the camera's tap-to-focus. setOpenTab(null); setOpenParamKey(null); - // A frame was picked for the still it sits on; camera mode has no FRAME - // picker to undo it, so it must never follow the user onto the live camera - // (and into the capture, which burns the frame in). + resetEditsToRecipe(); + // Camera mode has no FRAME picker to undo a frame, so even a recipe that + // carries one must not follow the user onto the live camera (and into the + // capture, which burns the frame in). if (next === 'camera') setSelectedFrame('none'); - // Same for the custom watermark: it was placed against THAT library photo, - // so it must not follow the user onto the live camera either — the mark - // goes back to off, and re-enabling + editing it is a deliberate choice. - if (next === 'camera') { - setCustomWm({ ...DEFAULT_CUSTOM_WM }); - setCustomWmApplied({ ...DEFAULT_CUSTOM_WM }); - } setMode(next); }; diff --git a/modules/recipescam-export/android/src/main/java/com/locphamtran/recipescamera/export/RecipescamExportModule.kt b/modules/recipescam-export/android/src/main/java/com/locphamtran/recipescamera/export/RecipescamExportModule.kt index f6f6076..4f99332 100644 --- a/modules/recipescam-export/android/src/main/java/com/locphamtran/recipescamera/export/RecipescamExportModule.kt +++ b/modules/recipescam-export/android/src/main/java/com/locphamtran/recipescamera/export/RecipescamExportModule.kt @@ -485,11 +485,11 @@ private fun applyTone(bmp: Bitmap, dr: Float, hl: Float, sh: Float) { val g = ((c shr 8) and 0xFF) / 255f val b = (c and 0xFF) / 255f val t = (0.2126f * r + 0.7152f * g + 0.0722f * b).coerceIn(0f, 1f) - // Wide, soft knees (see TONE_SKSL): HL rides the upper half, SH the lower. - val hlMask = smoothstep(0.45f, 1.00f, t) + // Wide, soft knees (see TONE_SKSL): HL rides the top, SH the lower half. + val hlMask = smoothstep(0.65f, 1.00f, t) val shMask = 1f - smoothstep(0.00f, 0.55f, t) // Pure additive luma shifts -> monotonic curve (no reversal on a ramp). - var o = t + hl * 0.32f * hlMask + sh * 0.34f * shMask + var o = t + hl * 0.22f * hlMask + sh * 0.34f * shMask // Dynamic range: gentle shadow lift + highlight roll (protect brights). o += dr * 0.12f * shMask * (1f - t) o -= dr * 0.18f * hlMask * t diff --git a/src/dev/nativeBenchProbe.ts b/src/dev/nativeBenchProbe.ts index da080c6..570b1b5 100644 --- a/src/dev/nativeBenchProbe.ts +++ b/src/dev/nativeBenchProbe.ts @@ -53,7 +53,7 @@ export async function nativeBenchProbe(): Promise { console.error('[BENCH] classic-neg-on-black matrix[0..4]=' + matrix.slice(0, 5).join(',')); // P2 tone parity on solid ramps (expected values computed from TONE_SKSL): - // white(255) hl=-1 -> 173; gray64 sh=+1 -> 113; white dr=1 -> 209; + // white(255) hl=-1 -> 199; gray64 sh=+1 -> 113; white dr=1 -> 209; // gray26 sh=+1 -> 105; gray26 hl=+1 sh=-1 -> 0. const toneGates: Array<{ rgb: [number, number, number]; tone: { dr: number; hl: number; sh: number }; tag: string }> = [ { rgb: [255, 255, 255], tone: { dr: 0, hl: -1, sh: 0 }, tag: 'white-hl-1' }, diff --git a/src/utils/toneShader.ts b/src/utils/toneShader.ts index 290d995..426ffea 100644 --- a/src/utils/toneShader.ts +++ b/src/utils/toneShader.ts @@ -9,14 +9,15 @@ import { ColorAdjustments } from '../types'; // turned saturated blues black under -SH and bright colours grey under -HL. // // Wide, soft knees so the knobs reach like a tone curve instead of biting only -// at the very ends: HL rides the upper half (0.45..1.00), SH the lower half -// (0.00..0.55), and the 0.50 midpoint moves less than 3%. Earlier narrow +// at the very ends: HL rides only the top (0.65..1.00) so it leaves the greys +// and midtones alone (a 0.45 knee dragged a mid-grey down) while SH rides the +// lower half (0.00..0.55), and the 0.50 midpoint never moves. Earlier narrow // knees (0.80..1.00 / 0.00..0.30) made both sliders look dead on any photo // without true whites or blacks. // // Both knobs are pure additive shifts of the luma. That keeps the curve -// monotonic (steepest knee slope 2.73/unit * 0.32 = 0.87, +SH overlap still -// positive), so a brighter input can never come out darker. The earlier +// monotonic (steepest knee slope 4.29/unit * 0.22 = 0.94, and the two knees +// never overlap), so a brighter input can never come out darker. The earlier // multiplicative form was NOT monotonic: with hl=-1 a grey 0.73 came out // darker than 0.80. // @@ -38,10 +39,10 @@ vec4 main(vec2 xy) { vec4 c = src.eval(xy); vec3 rgb = clamp(c.rgb, 0.0, 1.0); float t = clamp(dot(rgb, vec3(0.2126, 0.7152, 0.0722)), 0.0, 1.0); - float hlMask = smoothstep(0.45, 1.00, t); + float hlMask = smoothstep(0.65, 1.00, t); float shMask = 1.0 - smoothstep(0.00, 0.55, t); // NOTE: never name a local 'out' — it is a reserved SkSL qualifier. - float o = t + hl * 0.32 * hlMask + sh * 0.34 * shMask; + float o = t + hl * 0.22 * hlMask + sh * 0.34 * shMask; // Dynamic range: gentle shadow lift + highlight roll (protect brights). o += dr * 0.12 * shMask * (1.0 - t); o -= dr * 0.18 * hlMask * t;