feat: cinematic seasonal atmosphere presets (spring/summer/autumn/winter) - enhances existing light with season tints, sun glow, fog veil
This commit is contained in:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user