|
|
|
@@ -90,6 +90,11 @@ interface ViewfinderProps {
|
|
|
|
|
// Session-level error from the camera (e.g. DNG output failed to bind). App
|
|
|
|
|
// auto-disables RAW on such errors and alerts the user.
|
|
|
|
|
onSessionError?: (error: Error) => void;
|
|
|
|
|
// Live session ready flag for the shutter gate: true once the CameraX
|
|
|
|
|
// session is bound (onStarted), false when it goes down (onStopped). App
|
|
|
|
|
// queues a shutter press that lands before it — firing one into a dead
|
|
|
|
|
// session produced no file at all.
|
|
|
|
|
onCameraReady?: (ready: boolean) => void;
|
|
|
|
|
// Quick exposure slider (shown while AE/AF is locked) — live AE bias in EV.
|
|
|
|
|
onExposureChange?: (ev: number) => void;
|
|
|
|
|
// Metering-mode tonal look (highlight-weighted): extra adjustments merged on
|
|
|
|
@@ -197,6 +202,23 @@ export interface ViewfinderHandle {
|
|
|
|
|
// the old per-pixel hash, since the smoothed cell fraction is 0 there. The
|
|
|
|
|
// bilinear over the four cell corners is what makes a clump instead of a pixel
|
|
|
|
|
// of static — at cell 1 it collapses back to the plain hash.
|
|
|
|
|
// Every Skia object the per-frame worklet builds (paint, filter, shader) is a
|
|
|
|
|
// native allocation; the UI runtime's GC does not reach them promptly, so a
|
|
|
|
|
// preview left open piles up ~24 MB/minute that the device pays back as swap.
|
|
|
|
|
// The frame pushes each object here and this releases them after the last draw
|
|
|
|
|
// that could still reference them. Reverse order: children were handed refs by
|
|
|
|
|
// the parents built after them.
|
|
|
|
|
type SkTrash = { dispose: () => void };
|
|
|
|
|
const disposeTrash = (trash: SkTrash[]) => {
|
|
|
|
|
'worklet';
|
|
|
|
|
for (let i = trash.length - 1; i >= 0; i--) {
|
|
|
|
|
try {
|
|
|
|
|
trash[i].dispose();
|
|
|
|
|
} catch {
|
|
|
|
|
// A dispose failure must never take the frame down with it.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const noiseEffect = Skia.RuntimeEffect.Make(`
|
|
|
|
|
uniform float u;
|
|
|
|
|
float grainHash(vec2 q) {
|
|
|
|
@@ -291,6 +313,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
rawEnabled,
|
|
|
|
|
rawOutput,
|
|
|
|
|
onSessionError,
|
|
|
|
|
onCameraReady,
|
|
|
|
|
onExposureChange,
|
|
|
|
|
meteringAdjustments,
|
|
|
|
|
imageAdjustTarget,
|
|
|
|
@@ -533,10 +556,12 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
watchdogBudgetRef.current = 0;
|
|
|
|
|
setCameraActive(true);
|
|
|
|
|
bumpSessionStart((n) => n + 1);
|
|
|
|
|
}, []);
|
|
|
|
|
onCameraReady?.(true);
|
|
|
|
|
}, [onCameraReady]);
|
|
|
|
|
const handleCameraStopped = useCallback(() => {
|
|
|
|
|
setCameraActive(false);
|
|
|
|
|
}, []);
|
|
|
|
|
onCameraReady?.(false);
|
|
|
|
|
}, [onCameraReady]);
|
|
|
|
|
// Camera session pauses when the app backgrounds; toggling isActive back on
|
|
|
|
|
// resume restarts it. Without this the preview stayed black after unlock.
|
|
|
|
|
const [appActive, setAppActive] = useState(AppState.currentState === 'active');
|
|
|
|
@@ -1168,12 +1193,17 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
// read can throw inside MakeMatrix (or yield NaN -> black surface) for
|
|
|
|
|
// one frame. Validate + try/catch, fall back to identity (no filter):
|
|
|
|
|
// a 1-frame gain error beats a 1-frame black beat. DEBUG-EV.
|
|
|
|
|
const paint = Skia.Paint();
|
|
|
|
|
const trash: SkTrash[] = [];
|
|
|
|
|
const keep = <T extends SkTrash>(o: T): T => {
|
|
|
|
|
trash.push(o);
|
|
|
|
|
return o;
|
|
|
|
|
};
|
|
|
|
|
const paint = keep(Skia.Paint());
|
|
|
|
|
let baseColorFilter: ReturnType<typeof Skia.ColorFilter.MakeMatrix> | null = null;
|
|
|
|
|
try {
|
|
|
|
|
const cm = colorMatrixSync.getDirty();
|
|
|
|
|
if (cm != null && cm.length === 20 && cm.every((v) => typeof v === 'number' && Number.isFinite(v))) {
|
|
|
|
|
baseColorFilter = Skia.ColorFilter.MakeMatrix(cm);
|
|
|
|
|
baseColorFilter = keep(Skia.ColorFilter.MakeMatrix(cm));
|
|
|
|
|
paint.setColorFilter(baseColorFilter);
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('Invalid color matrix (len ' + (cm == null ? 'null' : cm.length) + ') — filter skipped');
|
|
|
|
@@ -1194,7 +1224,8 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
const a = (cv / 10) * 0.8;
|
|
|
|
|
let lensFilter =
|
|
|
|
|
cv > 0
|
|
|
|
|
? Skia.ImageFilter.MakeMatrixConvolution(
|
|
|
|
|
? keep(
|
|
|
|
|
Skia.ImageFilter.MakeMatrixConvolution(
|
|
|
|
|
3,
|
|
|
|
|
3,
|
|
|
|
|
[0, -a, 0, -a, 1 + 4 * a, -a, 0, -a, 0],
|
|
|
|
@@ -1205,21 +1236,26 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
T_CLAMP,
|
|
|
|
|
true,
|
|
|
|
|
null
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
: cv < 0
|
|
|
|
|
? Skia.ImageFilter.MakeBlur(
|
|
|
|
|
Math.abs(cv / 10) * 4,
|
|
|
|
|
Math.abs(cv / 10) * 4,
|
|
|
|
|
T_CLAMP,
|
|
|
|
|
null
|
|
|
|
|
? keep(
|
|
|
|
|
Skia.ImageFilter.MakeBlur(
|
|
|
|
|
Math.abs(cv / 10) * 4,
|
|
|
|
|
Math.abs(cv / 10) * 4,
|
|
|
|
|
T_CLAMP,
|
|
|
|
|
null
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
: null;
|
|
|
|
|
if (sv < 0) {
|
|
|
|
|
lensFilter = Skia.ImageFilter.MakeBlur(
|
|
|
|
|
Math.abs(sv / 10) * 3,
|
|
|
|
|
Math.abs(sv / 10) * 3,
|
|
|
|
|
T_CLAMP,
|
|
|
|
|
lensFilter
|
|
|
|
|
lensFilter = keep(
|
|
|
|
|
Skia.ImageFilter.MakeBlur(
|
|
|
|
|
Math.abs(sv / 10) * 3,
|
|
|
|
|
Math.abs(sv / 10) * 3,
|
|
|
|
|
T_CLAMP,
|
|
|
|
|
lensFilter
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (lensFilter != null) paint.setImageFilter(lensFilter);
|
|
|
|
@@ -1244,13 +1280,13 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
let fullShader = null;
|
|
|
|
|
if (hasTone || hasCinema) {
|
|
|
|
|
try {
|
|
|
|
|
const imageShader = frameTexture.makeShaderOptions(T_CLAMP, T_CLAMP, F_LINEAR, M_NONE);
|
|
|
|
|
const imageShader = keep(frameTexture.makeShaderOptions(T_CLAMP, T_CLAMP, F_LINEAR, M_NONE));
|
|
|
|
|
let final = null;
|
|
|
|
|
if (hasTone) {
|
|
|
|
|
final = toneEffect.makeShaderWithChildren(tone, [imageShader]);
|
|
|
|
|
final = keep(toneEffect.makeShaderWithChildren(tone, [imageShader]));
|
|
|
|
|
}
|
|
|
|
|
if (hasCinema) {
|
|
|
|
|
const cinemaShader = cinemaEffect.makeShaderWithChildren(cinema, [final ?? imageShader]);
|
|
|
|
|
const cinemaShader = keep(cinemaEffect.makeShaderWithChildren(cinema, [final ?? imageShader]));
|
|
|
|
|
if (cinemaShader != null) final = cinemaShader;
|
|
|
|
|
}
|
|
|
|
|
if (final != null) fullShader = final;
|
|
|
|
@@ -1283,10 +1319,10 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
const srcShader =
|
|
|
|
|
fullShader != null
|
|
|
|
|
? fullShader
|
|
|
|
|
: frameTexture.makeShaderOptions(T_CLAMP, T_CLAMP, F_LINEAR, M_NONE);
|
|
|
|
|
: keep(frameTexture.makeShaderOptions(T_CLAMP, T_CLAMP, F_LINEAR, M_NONE));
|
|
|
|
|
const glowShader =
|
|
|
|
|
glowEffect != null ? glowEffect.makeShaderWithChildren(glowUniformArray(), [srcShader]) : null;
|
|
|
|
|
const glowPaint = Skia.Paint();
|
|
|
|
|
glowEffect != null ? keep(glowEffect.makeShaderWithChildren(glowUniformArray(), [srcShader])) : null;
|
|
|
|
|
const glowPaint = keep(Skia.Paint());
|
|
|
|
|
glowPaint.setBlendMode(BlendMode.Screen);
|
|
|
|
|
glowPaint.setAlphaf(0.15 + 0.35 * strength);
|
|
|
|
|
if (glowShader != null) {
|
|
|
|
@@ -1294,10 +1330,15 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
// Recipe matrix stays where it always was — inside the filter
|
|
|
|
|
// chain, under the blur — so the bloom lands on GRADED pixels
|
|
|
|
|
// (a monochrome recipe glows white, not colour).
|
|
|
|
|
const glowBlur = Skia.ImageFilter.MakeBlur(sigma, sigma, TileMode.Clamp, null);
|
|
|
|
|
const glowBlur = keep(Skia.ImageFilter.MakeBlur(sigma, sigma, TileMode.Clamp, null));
|
|
|
|
|
glowPaint.setImageFilter(
|
|
|
|
|
baseColorFilter != null
|
|
|
|
|
? Skia.ImageFilter.MakeCompose(glowBlur, Skia.ImageFilter.MakeColorFilter(baseColorFilter, null))
|
|
|
|
|
? keep(
|
|
|
|
|
Skia.ImageFilter.MakeCompose(
|
|
|
|
|
glowBlur,
|
|
|
|
|
keep(Skia.ImageFilter.MakeColorFilter(baseColorFilter, null))
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
: glowBlur
|
|
|
|
|
);
|
|
|
|
|
canvas.drawRect(Skia.XYWHRect(0, 0, width, frameTexture.height()), glowPaint);
|
|
|
|
@@ -1312,16 +1353,22 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
];
|
|
|
|
|
// Order is load-bearing (see exportEngine pass 5b): chain both as
|
|
|
|
|
// image filters so the highlight selection runs BEFORE the blur.
|
|
|
|
|
const selection = Skia.ImageFilter.MakeColorFilter(
|
|
|
|
|
baseColorFilter != null
|
|
|
|
|
? Skia.ColorFilter.MakeCompose(Skia.ColorFilter.MakeMatrix(hiMatrix), baseColorFilter)
|
|
|
|
|
: Skia.ColorFilter.MakeMatrix(hiMatrix),
|
|
|
|
|
null
|
|
|
|
|
const selection = keep(
|
|
|
|
|
Skia.ImageFilter.MakeColorFilter(
|
|
|
|
|
baseColorFilter != null
|
|
|
|
|
? keep(
|
|
|
|
|
Skia.ColorFilter.MakeCompose(keep(Skia.ColorFilter.MakeMatrix(hiMatrix)), baseColorFilter)
|
|
|
|
|
)
|
|
|
|
|
: keep(Skia.ColorFilter.MakeMatrix(hiMatrix)),
|
|
|
|
|
null
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
glowPaint.setImageFilter(
|
|
|
|
|
Skia.ImageFilter.MakeCompose(
|
|
|
|
|
Skia.ImageFilter.MakeBlur(sigma, sigma, TileMode.Clamp, null),
|
|
|
|
|
selection
|
|
|
|
|
keep(
|
|
|
|
|
Skia.ImageFilter.MakeCompose(
|
|
|
|
|
keep(Skia.ImageFilter.MakeBlur(sigma, sigma, TileMode.Clamp, null)),
|
|
|
|
|
selection
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
if (fullShader != null) {
|
|
|
|
@@ -1347,7 +1394,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
const bandActive = feed[0] > 0;
|
|
|
|
|
if (!bandActive) {
|
|
|
|
|
drawFullFrame();
|
|
|
|
|
return;
|
|
|
|
|
return disposeTrash(trash);
|
|
|
|
|
}
|
|
|
|
|
const isLand = orient === 'left' || orient === 'right';
|
|
|
|
|
const ow = isLand ? ftH : ftW;
|
|
|
|
@@ -1356,7 +1403,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
const viewAspect = feed[1];
|
|
|
|
|
if (!(ow > 0 && oh > 0 && ac > 0 && viewAspect > 0 && feed[4] > 0 && feed[5] > 0)) {
|
|
|
|
|
drawFullFrame();
|
|
|
|
|
return;
|
|
|
|
|
return disposeTrash(trash);
|
|
|
|
|
}
|
|
|
|
|
// View -> upright-snapshot mapping: the snapshot is cover-fitted to the
|
|
|
|
|
// view, so the view shows a centered visX x visY window of it.
|
|
|
|
@@ -1407,7 +1454,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
const rh = v1 - v0;
|
|
|
|
|
if (!(rw > 0 && rh > 0)) {
|
|
|
|
|
drawFullFrame();
|
|
|
|
|
return;
|
|
|
|
|
return disposeTrash(trash);
|
|
|
|
|
}
|
|
|
|
|
// Cover-map the full sensor into the raw rect (dst preserves the sensor
|
|
|
|
|
// aspect, centered; the clip below trims the overflow) — translate then
|
|
|
|
@@ -1439,7 +1486,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
const k = s * z;
|
|
|
|
|
// Fresh paint; its default color is opaque black (Skia default), so
|
|
|
|
|
// the surface fill below needs no explicit color set.
|
|
|
|
|
const blackPaint = Skia.Paint();
|
|
|
|
|
const blackPaint = keep(Skia.Paint());
|
|
|
|
|
canvas.drawRect(Skia.XYWHRect(0, 0, ftW, ftH), blackPaint);
|
|
|
|
|
canvas.save();
|
|
|
|
|
canvas.clipRect(Skia.XYWHRect(u0, v0, rw, rh), CLIP_INTERSECT, false);
|
|
|
|
@@ -1447,6 +1494,7 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
|
|
|
|
|
canvas.scale(k, k);
|
|
|
|
|
drawFullFrame();
|
|
|
|
|
canvas.restore();
|
|
|
|
|
disposeTrash(trash);
|
|
|
|
|
});
|
|
|
|
|
frame.dispose();
|
|
|
|
|
},
|
|
|
|
|