Move the CROP band, not the picture

Library CROP now resizes the band in photo fractions, the way FREE always
did, instead of panning a re-scaled photo under a screen-sized rectangle:
what the band encloses is the export, with no zoom transform left to undo
and no aspect that only holds on a square photo. The band is laid out
below the top strip, whose height TopBar now reports, so it can never
peek under the header. The grain hashes a cell of width/1080 instead of
the raw pixel grid, so a 4000px export is no longer ~4x finer than the
preview that tuned it.
This commit is contained in:
2026-09-16 17:17:36 +07:00
parent 8f615f6cd7
commit 7b013fb6f8
4 changed files with 242 additions and 84 deletions
+19 -4
View File
@@ -457,14 +457,29 @@ export async function processAndExportPhoto(
grainPaint.setBlendMode(BlendMode.Overlay);
grainPaint.setAlphaf(grainOpacity);
// Procedural noise shader
// Procedural noise shader — byte-for-byte the preview's (see
// Viewfinder's noiseEffect). The cell is `width / 1080` so the grain is
// the same size RELATIVE to the picture as in the preview that tuned it;
// hashing the raw pixel grid made a 4000px file's grain ~4x finer than
// the grain the user had just tuned at 1080px.
const noiseEffect = Skia.RuntimeEffect.Make(`
uniform float u;
float grainHash(vec2 q) {
return fract(sin(dot(q, vec2(12.9898, 78.233))) * 43758.5453);
}
vec4 main(vec2 pos) {
float r = fract(sin(dot(pos.xy, vec2(12.9898, 78.233))) * 43758.5453);
return vec4(vec3(r), 1.0);
vec2 p = pos.xy / max(u, 1.0);
vec2 i = floor(p);
vec2 f = p - i;
f = f * f * (3.0 - 2.0 * f);
float g00 = grainHash(i);
float g10 = grainHash(i + vec2(1.0, 0.0));
float g01 = grainHash(i + vec2(0.0, 1.0));
float g11 = grainHash(i + vec2(1.0, 1.0));
return vec4(vec3(mix(mix(g00, g10, f.x), mix(g01, g11, f.x), f.y)), 1.0);
}
`);
const noiseShader = noiseEffect ? noiseEffect.makeShader([]) : null;
const noiseShader = noiseEffect ? noiseEffect.makeShader([width / 1080]) : null;
if (noiseShader) {
grainPaint.setShader(noiseShader);