Sell PRO as an in-app unlock, take shared photos, and let FX run negative

- One free Play app (com.locphamtran.recipescamera) with an expo-iap PRO
  unlock, replacing the paid apk for the store channel
- Android SEND/SEND_MULTIPLE share target feeds photos into the editor,
  on cold start and on resume
- BACK button in the TopBar and press-and-hold PEEK replace the chip row
- NOISE REDUCTION and SHARPENING accept -10: a negative SHARPENING softens
  the frame before the grain pass, a negative NOISE REDUCTION re-grains it,
  in the Skia engine, the live preview and the Kotlin export path alike
This commit is contained in:
2026-09-16 06:48:22 +07:00
parent 27a9dfb65f
commit a85ebf0a81
21 changed files with 860 additions and 183 deletions
+34 -4
View File
@@ -335,6 +335,24 @@ export async function processAndExportPhoto(
}
}
// Negative SHARPENING is the amount of digital edge enhancement to take
// back OUT. It runs HERE, inside the photo's own filter chain, because the
// post-grain twin (step 9) blurred the grain overlay away: NR < 0 plus
// SHARPENING < 0 exported as flat mush with no grain at all.
const sharpKnob = adjustments.sharpening ?? 0;
if (sharpKnob < 0) {
const softenSigma = (Math.abs(sharpKnob) / 10) * 3;
const softenFilter = Skia.ImageFilter.MakeBlur(
softenSigma,
softenSigma,
TileMode.Clamp,
imageFilter
);
if (softenFilter) {
imageFilter = softenFilter;
}
}
if (imageFilter) {
paint.setImageFilter(imageFilter);
}
@@ -423,8 +441,18 @@ export async function processAndExportPhoto(
// 6. Add Monochrome Grain Overlay
if (adjustments.grain > 0) {
const grainOpacity = adjustments.grain / 20; // up to 0.5 opacity
// NOISE REDUCTION below 0 works the other way round: it hands back the grain
// the denoiser would have flattened (a JPEG cannot be un-smoothed, but it
// can be re-grained). Same overlay as GRAIN, capped at the GRAIN slider's
// own ceiling so a negative NR plus GRAIN never double up.
// ponytail: re-grain, not de-noise; add a real grain-preserving NR the day
// the pipeline gets a pre-ISP raw frame.
const grainAmount = Math.min(
10,
Math.max(0, adjustments.grain) + Math.max(0, -(adjustments.denoise ?? 0))
);
if (grainAmount > 0) {
const grainOpacity = grainAmount / 20; // up to 0.5 opacity
const grainPaint = Skia.Paint();
grainPaint.setBlendMode(BlendMode.Overlay);
grainPaint.setAlphaf(grainOpacity);
@@ -732,8 +760,10 @@ export async function processAndExportPhoto(
let resultImage = outputSurface.makeImageSnapshot();
// Sharpening knob (0..10) overrides the legacy boolean sharpen flag:
// legacy camera captures keep sharpen:true → 0.5 fallback when knob is 0.
const sharpKnob = adjustments.sharpening ?? 0;
const sharpenAmount = sharpKnob > 0 ? (sharpKnob / 10) * 0.8 : options?.sharpen ? 0.5 : 0;
// A negative knob means "no sharpening at all" (its blur already ran at
// step 4, under the grain), so the legacy 0.5 fallback must not fire.
const sharpenAmount =
sharpKnob > 0 ? (sharpKnob / 10) * 0.8 : sharpKnob < 0 ? 0 : options?.sharpen ? 0.5 : 0;
if (sharpenAmount > 0) {
resultImage = screenSharpenImage(resultImage, sharpenAmount);
}