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 { Recipe, GPSInfo, FrameId } from '../types';
|
||||||
import { getSkiaColorMatrix, applyExposureGain } from '../utils/colorUtils';
|
import { getSkiaColorMatrix, applyExposureGain } from '../utils/colorUtils';
|
||||||
import { TONE_SKSL, getToneUniforms } from '../utils/toneShader';
|
import { TONE_SKSL, getToneUniforms } from '../utils/toneShader';
|
||||||
|
import { CINEMA_SKSL, getCinemaUniforms, cinemaOffFlat } from '../utils/cinemaShader';
|
||||||
|
|
||||||
interface ViewfinderProps {
|
interface ViewfinderProps {
|
||||||
mode: 'camera' | 'library';
|
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
|
// Plain numeric primitives (captured by value into the preview worklet — the
|
||||||
// worklet runtime can't read RNSkia's enum namespace objects directly).
|
// worklet runtime can't read RNSkia's enum namespace objects directly).
|
||||||
const T_CLAMP = TileMode.Clamp;
|
const T_CLAMP = TileMode.Clamp;
|
||||||
@@ -127,10 +137,18 @@ export default function Viewfinder({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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(() => {
|
useEffect(() => {
|
||||||
colorMatrixSync.setBlocking(colorMatrix);
|
colorMatrixSync.setBlocking(colorMatrix);
|
||||||
toneSync.setBlocking(toneParams);
|
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
|
// Exposure compensation → hardware AE bias. Passed imperatively (never as a
|
||||||
// declarative prop): v5 calls setExposureBias while the session is still
|
// declarative prop): v5 calls setExposureBias while the session is still
|
||||||
@@ -293,18 +311,27 @@ export default function Viewfinder({
|
|||||||
const paint = Skia.Paint();
|
const paint = Skia.Paint();
|
||||||
paint.setColorFilter(Skia.ColorFilter.MakeMatrix(colorMatrixSync.getBlocking()));
|
paint.setColorFilter(Skia.ColorFilter.MakeMatrix(colorMatrixSync.getBlocking()));
|
||||||
const tone = toneSync.getBlocking();
|
const tone = toneSync.getBlocking();
|
||||||
|
const cinema = cinemaSync.getBlocking();
|
||||||
const hasTone = toneEffect != null && (tone[0] !== 0 || tone[1] !== 0 || tone[2] !== 0);
|
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 {
|
try {
|
||||||
const imageShader = frameTexture.makeShaderOptions(T_CLAMP, T_CLAMP, F_LINEAR, M_NONE);
|
const imageShader = frameTexture.makeShaderOptions(T_CLAMP, T_CLAMP, F_LINEAR, M_NONE);
|
||||||
const shader = toneEffect.makeShaderWithChildren(tone, [imageShader]);
|
// drawRect, not drawImage: paint shaders only apply to geometry
|
||||||
if (shader != null) {
|
// fills — drawImage ignores the paint shader, which is why the
|
||||||
// drawRect, not drawImage: paint shaders only apply to geometry
|
// DR/Highlight/Shadow tone never showed on the live preview.
|
||||||
// fills — drawImage ignores the paint shader, which is why the
|
let final = null;
|
||||||
// DR/Highlight/Shadow tone never showed on the live preview.
|
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 w = frameTexture.width();
|
||||||
const h = frameTexture.height();
|
const h = frameTexture.height();
|
||||||
paint.setShader(shader);
|
paint.setShader(final);
|
||||||
canvas.drawRect(Skia.XYWHRect(0, 0, w, h), paint);
|
canvas.drawRect(Skia.XYWHRect(0, 0, w, h), paint);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -319,7 +346,7 @@ export default function Viewfinder({
|
|||||||
});
|
});
|
||||||
frame.dispose();
|
frame.dispose();
|
||||||
},
|
},
|
||||||
[colorMatrixSync, toneSync, toneFailLogged]
|
[colorMatrixSync, toneSync, cinemaSync, toneFailLogged]
|
||||||
);
|
);
|
||||||
|
|
||||||
const grainOpacity = adjustments.grain / 20;
|
const grainOpacity = adjustments.grain / 20;
|
||||||
@@ -646,6 +673,8 @@ export default function Viewfinder({
|
|||||||
|
|
||||||
// Library mode — fullscreen, full-photo (contain), pinch/double-tap zoom.
|
// Library mode — fullscreen, full-photo (contain), pinch/double-tap zoom.
|
||||||
const toneOn = toneParams[0] !== 0 || toneParams[1] !== 0 || toneParams[2] !== 0;
|
const toneOn = toneParams[0] !== 0 || toneParams[1] !== 0 || toneParams[2] !== 0;
|
||||||
|
const cinemaOn = cinemaEffect != null && !!cinemaParams;
|
||||||
|
const useShaderPass = !!imageFitRect && ((toneEffect != null && toneOn) || cinemaOn);
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 relative overflow-hidden bg-black items-center justify-center">
|
<View className="flex-1 relative overflow-hidden bg-black items-center justify-center">
|
||||||
{libraryImageUri && skiaImage ? (
|
{libraryImageUri && skiaImage ? (
|
||||||
@@ -653,32 +682,62 @@ export default function Viewfinder({
|
|||||||
<Canvas style={StyleSheet.absoluteFill}>
|
<Canvas style={StyleSheet.absoluteFill}>
|
||||||
<Group matrix={libZoomMatrix}>
|
<Group matrix={libZoomMatrix}>
|
||||||
<ColorMatrix matrix={colorMatrix} />
|
<ColorMatrix matrix={colorMatrix} />
|
||||||
{toneEffect && toneOn && imageFitRect ? (
|
{useShaderPass ? (
|
||||||
// DR/Highlight/Shadow tone curve. The Rect must cover exactly
|
// DR/Highlight/Shadow tone + Cinema seasonal grade, both live
|
||||||
// the contain-fitted photo area (not the full canvas): a
|
// over the photo. The Rect must cover exactly the contain-fitted
|
||||||
// fullscreen ImageShader with fit="contain" clamps/smears the
|
// photo area (not the full canvas): a fullscreen ImageShader
|
||||||
// photo edges across the black letterbox (user-visible as "the
|
// with fit="contain" clamps/smears the photo edges across the
|
||||||
// background is a zoomed copy of the photo").
|
// black letterbox (user-visible as "the background is a zoomed
|
||||||
<Rect x={imageFitRect.dx} y={imageFitRect.dy} width={imageFitRect.dw} height={imageFitRect.dh}>
|
// copy of the photo"). Cinema samples the tone output when both
|
||||||
<Shader
|
// are on (chain: cinema -> tone -> image), matrix applies after.
|
||||||
source={toneEffect}
|
<Rect
|
||||||
uniforms={{ dr: toneParams[0], hl: toneParams[1], sh: toneParams[2] }}
|
x={imageFitRect!.dx}
|
||||||
>
|
y={imageFitRect!.dy}
|
||||||
<ImageShader
|
width={imageFitRect!.dw}
|
||||||
image={skiaImage}
|
height={imageFitRect!.dh}
|
||||||
fit="fill"
|
>
|
||||||
tx="clamp"
|
{cinemaOn ? (
|
||||||
ty="clamp"
|
<Shader source={cinemaEffect!} uniforms={cinemaParams!.uniforms}>
|
||||||
rect={rect(imageFitRect.dx, imageFitRect.dy, imageFitRect.dw, imageFitRect.dh)}
|
{toneEffect && toneOn ? (
|
||||||
/>
|
<Shader
|
||||||
</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>
|
</Rect>
|
||||||
) : (
|
) : (
|
||||||
<SkiaImage
|
<SkiaImage image={skiaImage} fit="contain" rect={rect(0, 0, vw, vh)} />
|
||||||
image={skiaImage}
|
|
||||||
fit="contain"
|
|
||||||
rect={rect(0, 0, vw, vh)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
{grainOpacity > 0 && noiseEffect && (
|
{grainOpacity > 0 && noiseEffect && (
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
export type FrameId = 'none' | 'polaroid' | 'classic-white' | 'cinematic';
|
export type FrameId = 'none' | 'polaroid' | 'classic-white' | 'cinematic';
|
||||||
|
|
||||||
|
// Cinematic seasonal atmosphere grade. Not a fake re-light: it reads the
|
||||||
|
// photo's EXISTING exposure — warm tint + soft glow on areas already lit,
|
||||||
|
// cool tint in shade, plus a seasonal atmospheric haze/fog veil.
|
||||||
|
export type CinemaSeason = 'spring' | 'summer' | 'autumn' | 'winter';
|
||||||
|
|
||||||
export interface ColorAdjustments {
|
export interface ColorAdjustments {
|
||||||
exposure: number; // -10 to +10 (mapped to matrix multiplier or offset)
|
exposure: number; // -10 to +10 (mapped to matrix multiplier or offset)
|
||||||
contrast: number; // -10 to +10
|
contrast: number; // -10 to +10
|
||||||
@@ -25,6 +30,7 @@ export interface Recipe {
|
|||||||
baseFilter: 'classic-neg' | 'provia' | 'velvia' | 'monochrome' | 'none';
|
baseFilter: 'classic-neg' | 'provia' | 'velvia' | 'monochrome' | 'none';
|
||||||
adjustments: ColorAdjustments;
|
adjustments: ColorAdjustments;
|
||||||
frameId?: FrameId; // e.g. 'none', 'polaroid', 'classic-white', 'cinematic'
|
frameId?: FrameId; // e.g. 'none', 'polaroid', 'classic-white', 'cinematic'
|
||||||
|
cinema?: CinemaSeason | null; // seasonal atmosphere grade (optional)
|
||||||
useGeotag: boolean;
|
useGeotag: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { CinemaSeason } from '../types';
|
||||||
|
|
||||||
|
// Cinematic seasonal atmosphere grade. Deliberately NOT a fake re-light: a
|
||||||
|
// still photo has no depth, so "moving the sun" would look plastic. Instead
|
||||||
|
// this reads the photo's EXISTING exposure and pushes it a certain way:
|
||||||
|
// - lit areas get the season's warm/golden tint + a soft sun glow (halation)
|
||||||
|
// - shadow side gets a complementary cool tint
|
||||||
|
// - a seasonal haze/fog veil blends toward a fog color (strongest in winter)
|
||||||
|
// That reads as "sunlight / morning mist" without inventing new light.
|
||||||
|
//
|
||||||
|
// Uniforms are 16 flat floats (scalar form keeps the JSX <Shader> uniforms
|
||||||
|
// object and the worklet/export flat arrays in the exact same order):
|
||||||
|
// strength, contrast, saturation, haze,
|
||||||
|
// warmR, warmG, warmB, warmAmt,
|
||||||
|
// coolR, coolG, coolB, coolAmt,
|
||||||
|
// fogR, fogG, fogB, fogAmt
|
||||||
|
export const CINEMA_SKSL = `
|
||||||
|
uniform shader src;
|
||||||
|
uniform float strength;
|
||||||
|
uniform float contrast;
|
||||||
|
uniform float saturation;
|
||||||
|
uniform float haze;
|
||||||
|
uniform float warmR;
|
||||||
|
uniform float warmG;
|
||||||
|
uniform float warmB;
|
||||||
|
uniform float warmAmt;
|
||||||
|
uniform float coolR;
|
||||||
|
uniform float coolG;
|
||||||
|
uniform float coolB;
|
||||||
|
uniform float coolAmt;
|
||||||
|
uniform float fogR;
|
||||||
|
uniform float fogG;
|
||||||
|
uniform float fogB;
|
||||||
|
uniform float fogAmt;
|
||||||
|
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);
|
||||||
|
vec3 r = c.rgb;
|
||||||
|
// Filmic-ish gentle contrast on the luma (keeps hue).
|
||||||
|
r = mix(r, clamp((r - 0.5) * (1.0 + contrast * strength) + 0.5, 0.0, 1.0), strength);
|
||||||
|
// Saturation.
|
||||||
|
float l = dot(r, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
r = mix(vec3(l), r, mix(1.0, saturation, strength));
|
||||||
|
// Season tints follow the EXISTING light: warm on lit areas, cool in shade.
|
||||||
|
float lit = smoothstep(0.30, 1.0, t);
|
||||||
|
float shad = 1.0 - smoothstep(0.0, 0.45, t);
|
||||||
|
vec3 warm = vec3(warmR, warmG, warmB);
|
||||||
|
vec3 cool = vec3(coolR, coolG, coolB);
|
||||||
|
r *= 1.0 + (warm - 1.0) * lit * warmAmt * strength;
|
||||||
|
r *= 1.0 + (cool - 1.0) * shad * coolAmt * strength;
|
||||||
|
// Soft sun glow at the brightest points (halation of existing highlights).
|
||||||
|
float glow = smoothstep(0.65, 1.0, t);
|
||||||
|
r += warm * glow * warmAmt * 0.45 * strength;
|
||||||
|
// Atmospheric haze/fog veil (lightest shadow side => misty air).
|
||||||
|
vec3 fog = vec3(fogR, fogG, fogB);
|
||||||
|
r = mix(r, fog, fogAmt * strength * (0.3 + 0.7 * shad));
|
||||||
|
return vec4(clamp(r, 0.0, 1.0), c.a);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface CinemaUniformSet {
|
||||||
|
season: CinemaSeason;
|
||||||
|
// 16 flat floats — order matches CINEMA_SKSL declaration order above.
|
||||||
|
flat: number[];
|
||||||
|
// Name-keyed, for the JSX <Shader uniforms={...}> form.
|
||||||
|
uniforms: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZERO: Record<string, number> = {};
|
||||||
|
|
||||||
|
const SEASONS: Record<CinemaSeason, { name: string; values: number[] }> = {
|
||||||
|
// Soft fresh light, gentle warm haze, barely-there cool in shade.
|
||||||
|
spring: {
|
||||||
|
name: 'CINEMA SPRING',
|
||||||
|
values: [
|
||||||
|
0.6, 0.08, 1.04, 0.18,
|
||||||
|
1.02, 0.998, 0.95, 0.3,
|
||||||
|
0.97, 0.995, 1.03, 0.12,
|
||||||
|
0.95, 0.96, 0.98, 0.16,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Golden hour-ish high sun: strong warm on lit, bluish shade, crisp.
|
||||||
|
summer: {
|
||||||
|
name: 'CINEMA SUMMER',
|
||||||
|
values: [
|
||||||
|
0.75, 0.2, 1.12, 0.1,
|
||||||
|
1.08, 1.04, 0.88, 0.4,
|
||||||
|
0.93, 0.97, 1.06, 0.2,
|
||||||
|
1.0, 0.97, 0.88, 0.05,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Amber low sun + warm atmospheric haze.
|
||||||
|
autumn: {
|
||||||
|
name: 'CINEMA AUTUMN',
|
||||||
|
values: [
|
||||||
|
0.8, 0.16, 1.1, 0.24,
|
||||||
|
1.12, 1.02, 0.78, 0.42,
|
||||||
|
1.02, 0.98, 0.88, 0.16,
|
||||||
|
1.0, 0.93, 0.75, 0.18,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Cold winter fog: desaturated, blue-grey, heavy mist, no warm sun.
|
||||||
|
winter: {
|
||||||
|
name: 'CINEMA WINTER',
|
||||||
|
values: [
|
||||||
|
0.65, -0.02, 0.88, 0.38,
|
||||||
|
0.99, 0.995, 1.02, 0.06,
|
||||||
|
0.94, 0.97, 1.08, 0.3,
|
||||||
|
0.9, 0.93, 0.97, 0.45,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEYS = [
|
||||||
|
'strength', 'contrast', 'saturation', 'haze',
|
||||||
|
'warmR', 'warmG', 'warmB', 'warmAmt',
|
||||||
|
'coolR', 'coolG', 'coolB', 'coolAmt',
|
||||||
|
'fogR', 'fogG', 'fogB', 'fogAmt',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function cinemaSeasonName(season: CinemaSeason | null | undefined): string | null {
|
||||||
|
return season ? SEASONS[season]?.name ?? null : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCinemaUniforms(season: CinemaSeason | null | undefined): CinemaUniformSet | null {
|
||||||
|
if (!season) return null;
|
||||||
|
const s = SEASONS[season];
|
||||||
|
if (!s) return null;
|
||||||
|
const uniforms: Record<string, number> = {};
|
||||||
|
KEYS.forEach((k, i) => (uniforms[k] = s.values[i]));
|
||||||
|
return { season, flat: s.values, uniforms };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeroed 16-float block — used to turn the cinema pass OFF in the worklet
|
||||||
|
// synchronizable (strength === 0 means inactive there).
|
||||||
|
export function cinemaOffFlat(): number[] {
|
||||||
|
return [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cinemaIsActive(flat: number[] | null | undefined): boolean {
|
||||||
|
return !!flat && flat.length === 16 && flat[0] > 0;
|
||||||
|
}
|
||||||
@@ -142,4 +142,105 @@ export const DEFAULT_RECIPES: Recipe[] = [
|
|||||||
frameId: 'classic-white',
|
frameId: 'classic-white',
|
||||||
useGeotag: true,
|
useGeotag: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'cinema-spring-default',
|
||||||
|
name: 'CINEMA SPRING',
|
||||||
|
baseFilter: 'none',
|
||||||
|
adjustments: {
|
||||||
|
exposure: 0,
|
||||||
|
contrast: 1,
|
||||||
|
saturation: 1,
|
||||||
|
temperature: 5800,
|
||||||
|
tint: 0,
|
||||||
|
highlight: -1,
|
||||||
|
shadow: 1,
|
||||||
|
denoise: 2,
|
||||||
|
clarity: 1,
|
||||||
|
grain: 3,
|
||||||
|
colorChrome: 'none',
|
||||||
|
colorChromeBlue: 'none',
|
||||||
|
dynamicRange: 'auto',
|
||||||
|
sharpening: 0,
|
||||||
|
exposureCompensation: 0,
|
||||||
|
},
|
||||||
|
frameId: 'none',
|
||||||
|
cinema: 'spring',
|
||||||
|
useGeotag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cinema-summer-default',
|
||||||
|
name: 'CINEMA SUMMER',
|
||||||
|
baseFilter: 'none',
|
||||||
|
adjustments: {
|
||||||
|
exposure: 0,
|
||||||
|
contrast: 2,
|
||||||
|
saturation: 2,
|
||||||
|
temperature: 6000,
|
||||||
|
tint: -1,
|
||||||
|
highlight: 1,
|
||||||
|
shadow: -1,
|
||||||
|
denoise: 1,
|
||||||
|
clarity: 2,
|
||||||
|
grain: 2,
|
||||||
|
colorChrome: 'weak',
|
||||||
|
colorChromeBlue: 'none',
|
||||||
|
dynamicRange: 'auto',
|
||||||
|
sharpening: 0,
|
||||||
|
exposureCompensation: 0,
|
||||||
|
},
|
||||||
|
frameId: 'none',
|
||||||
|
cinema: 'summer',
|
||||||
|
useGeotag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cinema-autumn-default',
|
||||||
|
name: 'CINEMA AUTUMN',
|
||||||
|
baseFilter: 'none',
|
||||||
|
adjustments: {
|
||||||
|
exposure: 0,
|
||||||
|
contrast: 1,
|
||||||
|
saturation: 2,
|
||||||
|
temperature: 7200,
|
||||||
|
tint: -2,
|
||||||
|
highlight: -2,
|
||||||
|
shadow: 1,
|
||||||
|
denoise: 2,
|
||||||
|
clarity: 0,
|
||||||
|
grain: 4,
|
||||||
|
colorChrome: 'weak',
|
||||||
|
colorChromeBlue: 'none',
|
||||||
|
dynamicRange: 'auto',
|
||||||
|
sharpening: 0,
|
||||||
|
exposureCompensation: 0,
|
||||||
|
},
|
||||||
|
frameId: 'none',
|
||||||
|
cinema: 'autumn',
|
||||||
|
useGeotag: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cinema-winter-default',
|
||||||
|
name: 'CINEMA WINTER',
|
||||||
|
baseFilter: 'none',
|
||||||
|
adjustments: {
|
||||||
|
exposure: 1,
|
||||||
|
contrast: -1,
|
||||||
|
saturation: -2,
|
||||||
|
temperature: 4500,
|
||||||
|
tint: 2,
|
||||||
|
highlight: -2,
|
||||||
|
shadow: 2,
|
||||||
|
denoise: 3,
|
||||||
|
clarity: -2,
|
||||||
|
grain: 3,
|
||||||
|
colorChrome: 'none',
|
||||||
|
colorChromeBlue: 'weak',
|
||||||
|
dynamicRange: 'auto',
|
||||||
|
sharpening: 0,
|
||||||
|
exposureCompensation: 0,
|
||||||
|
},
|
||||||
|
frameId: 'none',
|
||||||
|
cinema: 'winter',
|
||||||
|
useGeotag: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Asset } from 'expo-asset';
|
|||||||
import { Recipe, GPSInfo, FrameId } from '../types';
|
import { Recipe, GPSInfo, FrameId } from '../types';
|
||||||
import { getSkiaColorMatrix, applyExposureGain } from './colorUtils';
|
import { getSkiaColorMatrix, applyExposureGain } from './colorUtils';
|
||||||
import { TONE_SKSL, getToneUniforms, toneIsActive } from './toneShader';
|
import { TONE_SKSL, getToneUniforms, toneIsActive } from './toneShader';
|
||||||
|
import { CINEMA_SKSL, getCinemaUniforms, cinemaIsActive } from './cinemaShader';
|
||||||
import { drawFrameOnCanvas } from './frameUtils';
|
import { drawFrameOnCanvas } from './frameUtils';
|
||||||
import { patchJpegDpi } from './jpegDpi';
|
import { patchJpegDpi } from './jpegDpi';
|
||||||
|
|
||||||
@@ -152,6 +153,31 @@ export async function processAndExportPhoto(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3c. Cinema seasonal atmosphere grade (chain: cinema -> tone -> image).
|
||||||
|
// All-zero uniforms (no recipe.cinema) → skipped entirely.
|
||||||
|
const cinema = getCinemaUniforms(recipe.cinema);
|
||||||
|
let paintShader = toneShader;
|
||||||
|
if (cinema && cinemaIsActive(cinema.flat)) {
|
||||||
|
try {
|
||||||
|
const cinemaEffect = Skia.RuntimeEffect.Make(CINEMA_SKSL);
|
||||||
|
if (cinemaEffect) {
|
||||||
|
const child = toneShader ?? skImage.makeShaderOptions(
|
||||||
|
TileMode.Clamp,
|
||||||
|
TileMode.Clamp,
|
||||||
|
FilterMode.Linear,
|
||||||
|
MipmapMode.None
|
||||||
|
);
|
||||||
|
const cs = cinemaEffect.makeShaderWithChildren(cinema.flat, [child]);
|
||||||
|
if (cs) {
|
||||||
|
paintShader = cs;
|
||||||
|
paint.setShader(paintShader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[export] CINEMA_SKSL failed - skipped: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Denoise / Clarity Image Filters (export-only enhancements)
|
// 4. Denoise / Clarity Image Filters (export-only enhancements)
|
||||||
let imageFilter = null;
|
let imageFilter = null;
|
||||||
|
|
||||||
@@ -202,7 +228,7 @@ export async function processAndExportPhoto(
|
|||||||
// whole chain (tone → matrix → denoise/clarity) applies — but only when a
|
// whole chain (tone → matrix → denoise/clarity) applies — but only when a
|
||||||
// real shader was built; otherwise plain drawImage (never drawRect on a
|
// real shader was built; otherwise plain drawImage (never drawRect on a
|
||||||
// shader-less paint, which paints solid color).
|
// shader-less paint, which paints solid color).
|
||||||
if (toneShader) {
|
if (paintShader) {
|
||||||
canvas.drawRect(Skia.XYWHRect(0, 0, width, height), paint);
|
canvas.drawRect(Skia.XYWHRect(0, 0, width, height), paint);
|
||||||
} else {
|
} else {
|
||||||
canvas.drawImage(skImage, 0, 0, paint);
|
canvas.drawImage(skImage, 0, 0, paint);
|
||||||
|
|||||||
Reference in New Issue
Block a user