feat: cinematic seasonal atmosphere presets (spring/summer/autumn/winter) - enhances existing light with season tints, sun glow, fog veil

This commit is contained in:
2026-09-08 11:38:27 +07:00
parent ea281d7974
commit 588a36d05a
5 changed files with 368 additions and 34 deletions
+92 -33
View File
@@ -24,6 +24,7 @@ import { Lock } from 'lucide-react-native';
import { Recipe, GPSInfo, FrameId } from '../types';
import { getSkiaColorMatrix, applyExposureGain } from '../utils/colorUtils';
import { TONE_SKSL, getToneUniforms } from '../utils/toneShader';
import { CINEMA_SKSL, getCinemaUniforms, cinemaOffFlat } from '../utils/cinemaShader';
interface ViewfinderProps {
mode: 'camera' | 'library';
@@ -61,6 +62,15 @@ const toneEffect = (() => {
}
})();
const cinemaEffect = (() => {
try {
return Skia.RuntimeEffect.Make(CINEMA_SKSL);
} catch (e) {
console.error('[cinema] CINEMA_SKSL compile failed: ' + e);
return null;
}
})();
// Plain numeric primitives (captured by value into the preview worklet — the
// worklet runtime can't read RNSkia's enum namespace objects directly).
const T_CLAMP = TileMode.Clamp;
@@ -127,10 +137,18 @@ export default function Viewfinder({
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
// Cinema seasonal grade params (16 flat floats; all-zero = off).
const cinemaParams = recipe.cinema ? getCinemaUniforms(recipe.cinema) : null;
const cinemaSync = useMemo(
() => createSynchronizable<number[]>(cinemaOffFlat()),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(() => {
colorMatrixSync.setBlocking(colorMatrix);
toneSync.setBlocking(toneParams);
}, [colorMatrix, toneParams, colorMatrixSync, toneSync]);
cinemaSync.setBlocking(cinemaParams ? cinemaParams.flat : cinemaOffFlat());
}, [colorMatrix, toneParams, cinemaParams, colorMatrixSync, toneSync, cinemaSync]);
// Exposure compensation → hardware AE bias. Passed imperatively (never as a
// declarative prop): v5 calls setExposureBias while the session is still
@@ -293,18 +311,27 @@ export default function Viewfinder({
const paint = Skia.Paint();
paint.setColorFilter(Skia.ColorFilter.MakeMatrix(colorMatrixSync.getBlocking()));
const tone = toneSync.getBlocking();
const cinema = cinemaSync.getBlocking();
const hasTone = toneEffect != null && (tone[0] !== 0 || tone[1] !== 0 || tone[2] !== 0);
if (hasTone) {
const hasCinema = cinemaEffect != null && cinema[0] > 0;
if (hasTone || hasCinema) {
try {
const imageShader = frameTexture.makeShaderOptions(T_CLAMP, T_CLAMP, F_LINEAR, M_NONE);
const shader = toneEffect.makeShaderWithChildren(tone, [imageShader]);
if (shader != null) {
// drawRect, not drawImage: paint shaders only apply to geometry
// fills — drawImage ignores the paint shader, which is why the
// DR/Highlight/Shadow tone never showed on the live preview.
// drawRect, not drawImage: paint shaders only apply to geometry
// fills — drawImage ignores the paint shader, which is why the
// DR/Highlight/Shadow tone never showed on the live preview.
let final = null;
if (hasTone) {
final = toneEffect.makeShaderWithChildren(tone, [imageShader]);
}
if (hasCinema) {
const cinemaShader = cinemaEffect.makeShaderWithChildren(cinema, [final ?? imageShader]);
if (cinemaShader != null) final = cinemaShader;
}
if (final != null) {
const w = frameTexture.width();
const h = frameTexture.height();
paint.setShader(shader);
paint.setShader(final);
canvas.drawRect(Skia.XYWHRect(0, 0, w, h), paint);
return;
}
@@ -319,7 +346,7 @@ export default function Viewfinder({
});
frame.dispose();
},
[colorMatrixSync, toneSync, toneFailLogged]
[colorMatrixSync, toneSync, cinemaSync, toneFailLogged]
);
const grainOpacity = adjustments.grain / 20;
@@ -646,6 +673,8 @@ export default function Viewfinder({
// Library mode — fullscreen, full-photo (contain), pinch/double-tap zoom.
const toneOn = toneParams[0] !== 0 || toneParams[1] !== 0 || toneParams[2] !== 0;
const cinemaOn = cinemaEffect != null && !!cinemaParams;
const useShaderPass = !!imageFitRect && ((toneEffect != null && toneOn) || cinemaOn);
return (
<View className="flex-1 relative overflow-hidden bg-black items-center justify-center">
{libraryImageUri && skiaImage ? (
@@ -653,32 +682,62 @@ export default function Viewfinder({
<Canvas style={StyleSheet.absoluteFill}>
<Group matrix={libZoomMatrix}>
<ColorMatrix matrix={colorMatrix} />
{toneEffect && toneOn && imageFitRect ? (
// DR/Highlight/Shadow tone curve. The Rect must cover exactly
// the contain-fitted photo area (not the full canvas): a
// fullscreen ImageShader with fit="contain" clamps/smears the
// photo edges across the black letterbox (user-visible as "the
// background is a zoomed copy of the photo").
<Rect x={imageFitRect.dx} y={imageFitRect.dy} width={imageFitRect.dw} height={imageFitRect.dh}>
<Shader
source={toneEffect}
uniforms={{ dr: toneParams[0], hl: toneParams[1], sh: toneParams[2] }}
>
<ImageShader
image={skiaImage}
fit="fill"
tx="clamp"
ty="clamp"
rect={rect(imageFitRect.dx, imageFitRect.dy, imageFitRect.dw, imageFitRect.dh)}
/>
</Shader>
{useShaderPass ? (
// DR/Highlight/Shadow tone + Cinema seasonal grade, both live
// over the photo. The Rect must cover exactly the contain-fitted
// photo area (not the full canvas): a fullscreen ImageShader
// with fit="contain" clamps/smears the photo edges across the
// black letterbox (user-visible as "the background is a zoomed
// copy of the photo"). Cinema samples the tone output when both
// are on (chain: cinema -> tone -> image), matrix applies after.
<Rect
x={imageFitRect!.dx}
y={imageFitRect!.dy}
width={imageFitRect!.dw}
height={imageFitRect!.dh}
>
{cinemaOn ? (
<Shader source={cinemaEffect!} uniforms={cinemaParams!.uniforms}>
{toneEffect && toneOn ? (
<Shader
source={toneEffect}
uniforms={{ dr: toneParams[0], hl: toneParams[1], sh: toneParams[2] }}
>
<ImageShader
image={skiaImage}
fit="fill"
tx="clamp"
ty="clamp"
rect={rect(imageFitRect!.dx, imageFitRect!.dy, imageFitRect!.dw, imageFitRect!.dh)}
/>
</Shader>
) : (
<ImageShader
image={skiaImage}
fit="fill"
tx="clamp"
ty="clamp"
rect={rect(imageFitRect!.dx, imageFitRect!.dy, imageFitRect!.dw, imageFitRect!.dh)}
/>
)}
</Shader>
) : toneEffect && toneOn ? (
<Shader
source={toneEffect}
uniforms={{ dr: toneParams[0], hl: toneParams[1], sh: toneParams[2] }}
>
<ImageShader
image={skiaImage}
fit="fill"
tx="clamp"
ty="clamp"
rect={rect(imageFitRect!.dx, imageFitRect!.dy, imageFitRect!.dw, imageFitRect!.dh)}
/>
</Shader>
) : null}
</Rect>
) : (
<SkiaImage
image={skiaImage}
fit="contain"
rect={rect(0, 0, vw, vh)}
/>
<SkiaImage image={skiaImage} fit="contain" rect={rect(0, 0, vw, vh)} />
)}
</Group>
{grainOpacity > 0 && noiseEffect && (