From 4bb2bda5c895877e2413939c00b0a79813a8b68b Mon Sep 17 00:00:00 2001 From: locphamtran Date: Thu, 10 Sep 2026 20:57:19 +0700 Subject: [PATCH] fix(sim): rebuild the six film-sim color matrices to match the stock looks Provia/Velvia/Astia/Classic Chrome/Classic Neg/Acros each get their own primaries+cross-talk matrix measured against the pipeline, plus a split-tone stage (shadow/highlight tint uniforms) for Classic Chrome's teal and Classic Neg's green-cyan shadows vs warm highlights. --- src/components/Viewfinder.tsx | 47 +++++++++++++++-------- src/utils/colorUtils.ts | 72 +++++++++++++++++++++-------------- src/utils/exportEngine.ts | 9 +++-- src/utils/nativeExport.ts | 5 ++- src/utils/toneShader.ts | 59 +++++++++++++++++++++++++--- 5 files changed, 139 insertions(+), 53 deletions(-) diff --git a/src/components/Viewfinder.tsx b/src/components/Viewfinder.tsx index 59fdbd1..ef1755b 100644 --- a/src/components/Viewfinder.tsx +++ b/src/components/Viewfinder.tsx @@ -29,7 +29,7 @@ import { import { Lock } from 'lucide-react-native'; import { Recipe, GPSInfo, FrameId, AspectRatio, ColorAdjustments, ASPECT_RATIO_W_H, CropRatio, CropRect, CROP_W_H, DEFAULT_CROP_RECT, MIN_CROP_FRAC } from '../types'; import { getSkiaColorMatrix, applyExposureGain } from '../utils/colorUtils'; -import { TONE_SKSL, getToneUniforms } from '../utils/toneShader'; +import { TONE_SKSL, getToneUniforms, toneUniformArray } from '../utils/toneShader'; import { CINEMA_SKSL, getCinemaUniforms, cinemaOffFlat } from '../utils/cinemaShader'; import { polaroidLayout, wallframeLayout, WALLFRAME_PNG_W, WALLFRAME_PNG_H } from '../utils/frameUtils'; import { rotateSkImage90 } from '../utils/skiaImage'; @@ -392,13 +392,12 @@ const Viewfinder = forwardRef(function Viewfi // eslint-disable-next-line react-hooks/exhaustive-deps [] ); - // Tone uniforms [dr, hl, sh] for the preview worklet; all-zero = no tone pass. - const toneParams = (() => { - const u = getToneUniforms(adjustments); - return [u.dr, u.hl, u.sh, u.vib]; - })(); + // Tone uniforms [dr, hl, sh, vib, shadow split RGB, highlight split RGB] for + // the preview worklet; all-zero = no tone pass. A stock may keep the pass on + // with every user knob at 0 (Classic Neg split tone, Classic Chrome crush). + const toneParams = toneUniformArray(getToneUniforms(adjustments, recipe.baseFilter)); const toneSync = useMemo( - () => createSynchronizable([0, 0, 0]), + () => createSynchronizable(new Array(10).fill(0)), // eslint-disable-next-line react-hooks/exhaustive-deps [] ); @@ -820,7 +819,11 @@ const Viewfinder = forwardRef(function Viewfi } const tone = toneSync.getDirty(); const cinema = cinemaSync.getDirty(); - const hasTone = toneEffect != null && (tone[0] !== 0 || tone[1] !== 0 || tone[2] !== 0 || tone[3] !== 0); + const hasTone = + toneEffect != null && + (tone[0] !== 0 || tone[1] !== 0 || tone[2] !== 0 || tone[3] !== 0 || + tone[4] !== 0 || tone[5] !== 0 || tone[6] !== 0 || + tone[7] !== 0 || tone[8] !== 0 || tone[9] !== 0); const hasCinema = cinemaEffect != null && cinema[0] > 0; // Draws the full frame with the tone/cinema shader when active (drawRect, // not drawImage: paint shaders only apply to geometry fills — drawImage @@ -2135,8 +2138,20 @@ const Viewfinder = forwardRef(function Viewfi } // Library mode — fullscreen, full-photo (contain), pinch/double-tap zoom. - const toneOn = - toneParams[0] !== 0 || toneParams[1] !== 0 || toneParams[2] !== 0 || toneParams[3] !== 0; + const toneOn = toneParams.some((v) => v !== 0); + // One object for every below; keys must match TONE_SKSL's uniforms. + const toneUniforms = { + dr: toneParams[0], + hl: toneParams[1], + sh: toneParams[2], + vib: toneParams[3], + shTr: toneParams[4], + shTg: toneParams[5], + shTb: toneParams[6], + hlTr: toneParams[7], + hlTg: toneParams[8], + hlTb: toneParams[9], + }; const cinemaOn = cinemaEffect != null && !!cinemaParams; const useShaderPass = !!imageFitRect && ((toneEffect != null && toneOn) || cinemaOn); @@ -2164,7 +2179,7 @@ const Viewfinder = forwardRef(function Viewfi {toneEffect && toneOn ? ( @@ -2175,7 +2190,7 @@ const Viewfinder = forwardRef(function Viewfi ) : toneEffect && toneOn ? ( @@ -2244,7 +2259,7 @@ const Viewfinder = forwardRef(function Viewfi {toneEffect && toneOn ? ( @@ -2255,7 +2270,7 @@ const Viewfinder = forwardRef(function Viewfi ) : toneEffect && toneOn ? ( @@ -2321,7 +2336,7 @@ const Viewfinder = forwardRef(function Viewfi {toneEffect && toneOn ? ( (function Viewfi ) : toneEffect && toneOn ? ( >> = { + 'classic-chrome': { sh: -0.28 }, + 'classic-neg': { shT: [-0.018, 0.009, 0.013], hlT: [0.024, 0.008, -0.012] }, +}; + +export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter): 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 }; + const film = (baseFilter && FILM_TONE[baseFilter]) || {}; + const shT: [number, number, number] = film.shT ?? [0, 0, 0]; + const hlT: [number, number, number] = film.hlT ?? [0, 0, 0]; + return { dr, hl: hl + (film.hl ?? 0), sh: sh + (film.sh ?? 0), vib, shT, hlT }; +} + +// Flat uniform buffer for `makeShaderWithChildren` / `` — the +// order must match TONE_SKSL's declarations. +export function toneUniformArray(u: ToneUniforms): number[] { + return [u.dr, u.hl, u.sh, u.vib, u.shT[0], u.shT[1], u.shT[2], u.hlT[0], u.hlT[1], u.hlT[2]]; } export function toneIsActive(u: ToneUniforms): boolean { - return u.dr !== 0 || u.hl !== 0 || u.sh !== 0 || u.vib !== 0; + return ( + u.dr !== 0 || + u.hl !== 0 || + u.sh !== 0 || + u.vib !== 0 || + u.shT[0] !== 0 || + u.shT[1] !== 0 || + u.shT[2] !== 0 || + u.hlT[0] !== 0 || + u.hlT[1] !== 0 || + u.hlT[2] !== 0 + ); }