fix(heal): one drag is one repair, and the band at its rim is the model's
A press has to mean what it lands on. A drag is a run of spots and the run has a spine, so a press within half a brush radius of that spine takes the drag and dragging moves it, while a press in the outer half of the spots — where the run is not, and a new repair is wanted — lays a stroke against it. Before, a brush near a repaired one was read as a drag OF it: the second stroke was folded into the first and the first moved under the user. Spots carry the drag they came from (stroke), older recipes still answer to the old radius, and the outline of a repaired run is drawn idle so it can be taken hold of (app.css). A repair already made is part of the photo it was made in. The renderer now solves one drag at a time, in the order they were drawn, each against the frame the drags before it have already left — so a stroke laid beside a repaired one invents its pixels from a frame that no longer holds what the first took away, and the repair the user has already accepted does not move when they brush next to it. Measured (probe-seam.mjs, a second stroke laid 2.5 brush radii from the first, counted in levels over the first drag's own circles): 0 at the worst pixel, 0 of 19189 pixels moved by more than two. The ring the first repair left behind was the fill's and not the model's. The model's pixels were laid through the mask's own ramp, so the outer quarter of every spot — where the mask still weighs the fill in at up to full strength — was the fill's, and the fill is the light of the WHOLE box it was built in: a stroke a third of the frame long is filled by blurs up to sixteen brush radii wide, so what it brings there is the box's average and not the rim's. The model is the repair wherever it has one, so it now owns the spot out to 0.92 of its radius (HEAL_PATCH_STOPS) and the fill only meets the photo in the sliver the mask itself fades over. Measured with the same probe: 0.76 levels of mean change over the repair where it was 1.86, 8.5 at the worst pixel where it was 14.2, and the edge strength at the rim after the repair 0.92 where the photo's own edges around it are 0.5 (1.52 before) — the band that was 8 to 11 levels dark is gone. matchToRim goes with them: the relaxation it ran over every crop measures nothing the band above does not already do, and the probe's numbers are identical with it removed.
This commit is contained in:
@@ -53,11 +53,20 @@ export const HEAL_FILL_MAX_LEVELS = 8;
|
||||
export function readHeal(heal: HealSpot[] | undefined): HealSpot[] {
|
||||
if (!Array.isArray(heal)) return [];
|
||||
return heal
|
||||
.map((s) => ({
|
||||
x: Math.min(1, Math.max(0, num(s?.x, 0))),
|
||||
y: Math.min(1, Math.max(0, num(s?.y, 0))),
|
||||
r: Math.max(0, num(s?.r, 0)),
|
||||
}))
|
||||
.map((s) => {
|
||||
const spot: HealSpot = {
|
||||
x: Math.min(1, Math.max(0, num(s?.x, 0))),
|
||||
y: Math.min(1, Math.max(0, num(s?.y, 0))),
|
||||
r: Math.max(0, num(s?.r, 0)),
|
||||
};
|
||||
// Which drag the spot came from is carried through. The renderer solves
|
||||
// each drag as its own repair and in the order they were drawn, so each
|
||||
// one reads the repairs already made next to it rather than the photo
|
||||
// they were made over (exportEngine's heal step), and it can only do that
|
||||
// if the record of the gesture survives being read.
|
||||
if (typeof s?.stroke === 'number' && Number.isFinite(s.stroke)) spot.stroke = s.stroke;
|
||||
return spot;
|
||||
})
|
||||
.filter((s) => s.r > 0);
|
||||
}
|
||||
|
||||
@@ -97,9 +106,18 @@ half4 main(float2 pos) {
|
||||
`;
|
||||
|
||||
// The second pass: the fill laid back under the brush.
|
||||
// Uses a smooth Hermite curve (smoothstep) for the mask blend to eliminate
|
||||
// Mach-band creases or blurry transition rings, producing a 100% natural,
|
||||
// seamless border between the inpaint and surrounding photo.
|
||||
//
|
||||
// The weight the fill is mixed in at is the mask's own ramp, read over the WHOLE
|
||||
// of it: m is 1 out to HEAL_FEATHER of the radius and runs to 0 at its edge, so
|
||||
// the fill arrives exactly as far as the mask says it may. Reaching full weight
|
||||
// at 35% of the ramp instead — smoothstep(0, 0.35, m), which is what this shader
|
||||
// did first — crosses the fade in a third of the room the mask gives it, and the
|
||||
// repair then meets the photo along a line. Measured on the probe
|
||||
// (probe-seam.mjs, one stroke across a photo's own detail, ROW 0.75 and 0.85),
|
||||
// counting the pixels outside the brush that moved by more than six levels: 338
|
||||
// and 392 along that line, by up to 73 and 148 levels, 13 and 39 of them more
|
||||
// than a pixel and a half outside the rim. Read over the whole ramp: 66 and 57,
|
||||
// by up to 11 and 45, and none of them more than a pixel and a half outside.
|
||||
export const HEAL_FILL_SKSL = `
|
||||
uniform shader img;
|
||||
uniform shader fill;
|
||||
@@ -124,3 +142,23 @@ half4 main(float2 pos) {
|
||||
// brush turned down to three pixels and one at a quarter of the frame feather by
|
||||
// the same fraction of themselves.
|
||||
export const HEAL_MASK_STOPS: number[] = [0, HEAL_FEATHER, 1];
|
||||
|
||||
// The same stops for the model's own pixels, and they are not the mask's. The
|
||||
// model is the repair wherever it has one, so its shape is the whole spot and
|
||||
// the last fraction of a radius is the only place it gives way. At
|
||||
// HEAL_MASK_STOPS instead — the model solid out to 0.75 of the radius and faded
|
||||
// out by the rim — the band between was the FILL's, and the fill is built from
|
||||
// the frame's own light over the whole of its box (a stroke a third of the frame
|
||||
// long is filled by blurs up to sixteen brush radii wide, so what it brings into
|
||||
// that band is the box's average and not the rim's).
|
||||
//
|
||||
// Measured (probe-seam.mjs, one stroke of seven spots across the sky, `d` over
|
||||
// the spots' own circles between the frame before the repair and after it): the
|
||||
// band came back 8 to 11 levels below the photo it was made in — a ring, on the
|
||||
// one line a repair is looked at along. With the model owning the band: 0.71
|
||||
// levels of mean change over the whole repair, 8.5 at its worst pixel, and the
|
||||
// edge strength at the rim after the repair 0.84 where the photo's own edges
|
||||
// around it are 0.5 (1.52 before). Past 0.92 the numbers stop moving (0.73 /
|
||||
// 8.7 / 0.88 at 0.995), so the model does own the band and what is left there is
|
||||
// the shape of the spot, not the fill under it.
|
||||
export const HEAL_PATCH_STOPS: number[] = [0, 0.92, 1];
|
||||
|
||||
Reference in New Issue
Block a user