web: BLACK keeps the picture's texture and not the base it was read off, clarity stops the blur at an edge instead of at a colour, and a backup folder that refuses is handed back to the picker
BLACK at full deflection returned a soft picture, and on a monochrome frame it
returned the blurred base outright. The tone pass reads the ramp at the
neighbourhood's base and then rebuilds the pixel, and it rebuilt it by the RATIO
the base had moved by — `Base' * (Input / Base)`, the other half of
fix_shadow.md's decomposition. A ratio is a gain, and that gain is a function of
the neighbourhood: the knob that takes the base toward zero scales every pixel's
detail by the same coefficient, so the knob that darkens the frame takes its
texture with it. Measured on lightroom_shadow.jpg at 1024px through tone-sim.mjs,
the pass in plain JS with the shader's own constants: at BLACK -100 the finest
gradient came back at 0.75 of the input under the ratio and at 0.98 under the
sum, while the frame darkened the same either way (mean 0.416 to 0.359 both). A
monochrome stock is the whole frame of that error, because all three channels ARE
the pixel's luma there, which is why the picture came back as the base — soft,
and short of every edge it had.
The reconstruction is `Base' + Detail`, ADDED and not scaled, and it costs
nothing where there is no move to make: with every knob on zero the ramp at the
base IS the base, so the difference is exactly zero and the pass is the identity
however coarse the base is. A caller that hands in no neighbourhood at all — a
mask — hands in the pixel's own image as its base and gets the global move back,
which is what the ratio gave it too. `o` is held inside the cube before the
detail is added, so a neighbourhood the ramp has pushed under the floor keeps the
structure around it instead of carrying its pedestal down onto every pixel in the
region. The bright side of the same move is untouched: the lift is still the
neighbourhood's, and at SHADOW +100 the band above the lifted knot still keeps
0.81 of its spread where the global move kept 0.42.
CLARITY drew a light stroke down every contour, and the reason was in what the
blur called a neighbour. The range weight was the colour difference,
`exp(-dot(d, d) * 24)`, which is loose on any coloured edge — two sides of a hair,
a branch or a rail can share a red and differ in green — so the reference reached
across the edge, and the reference is exactly what the blend subtracts. The wider
it reaches, the more a contour reads as detail. It reads LUMINANCE now, one
decision per tap at the doc's own scale (`CLARITY_RANGE_SIGMA` = 0.04), so an edge
of any hue stops the blur dead.
The blend moved the three channels by their own differences, which is what
coloured fringing along every contour was, and the amount it moved them by was
the MASK's `CLARITY_GAIN` borrowed for a different child. It moves LUMINANCE now
— one value carries the whole pixel back with it, so hue is untouchable and skin
does not go sallow at the top of the knob — under the doc's midtone weight
M(L) = 4L(1-L), which deepens the greys a picture is made of and leaves the burnt
ends and the deepest shadows where they are. The positive side's gain lives
inside the shader (4.5, raised from the doc's 1.8 because the range weight above
reaches less far and carries less detail): clarity-halo.mjs, which measures the
pass pushing a pixel outside the range of its own neighbourhood, reads a bright
stroke of 55.3/255 on lightroom_shadow.jpg and 54.3/255 on DSCF1701.JPG at +10,
against the old pass's 145 and 127 at the same knob — and 8.0 already reads 98, so
the knob does not need to go further to keep the flat areas moving.
`exportEngine` passes the knob's own units now, `[clarityKnob / 10]`, since the
gain and the sign are the shader's business and the mask's gain is not the
frame's.
And a backup folder the browser had stopped letting the page write to wrote
nothing and said so, once, with no way back: a permission outlives the tab only
while the tab does, so the row kept reading a permission of a moment ago while
the write went to the disk without one. A refusal that names the permission, or
the folder that is not there, now goes through the picker ONCE — the picker is
the only thing that hands a folder back — and the run is repeated; anything else
is the folder itself saying no, and is not asked twice. The sentence on the
screen is one line over a strip of photographs and cannot carry a reason, so the
reason goes to the console and a word of it into the note (`{why}`, the name the
browser gave the refusal and never a stack), which is the whole of what tells a
folder that was moved from a permission that lapsed. Both dictionaries learn the
word.
Checked: `tsc --noEmit` clean; `tone-base-check.mjs` and `highlight-knee-check.mjs`
updated to the new reconstruction and passing; `tone-sim.mjs` on
lightroom_shadow.jpg at 1024px for the numbers above; `clarity-halo.mjs` for the
stroke.
This commit is contained in:
@@ -119,6 +119,20 @@ const shots = new Map<string, Promise<Shot | null>>();
|
||||
const CATALOGUE_MS = 5000;
|
||||
const CATCHUP_MS = 30000;
|
||||
|
||||
// The two refusals the write comes back with when the folder is not there or the
|
||||
// browser is not letting this page write to it: the names `libraryBackup` throws
|
||||
// them by, and the ones the picker is the answer to.
|
||||
const needsPick = (err: unknown) =>
|
||||
err instanceof Error && (err.message === 'no-backup-permission' || err.message === 'no-backup-folder');
|
||||
|
||||
// A word for the note's parenthesis: the name the browser gave the refusal, or
|
||||
// the short name the backup threw, and never a stack.
|
||||
const why = (err: unknown) => {
|
||||
if (err instanceof Error && err.message.startsWith('no-backup-')) return err.message.replace('no-backup-', '');
|
||||
if (typeof DOMException !== 'undefined' && err instanceof DOMException) return err.name;
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export function Library() {
|
||||
@@ -274,11 +288,31 @@ export function Library() {
|
||||
setBackBusy(true);
|
||||
if (!quiet) setNote(t('lib.backupRun', { done: 0, total: backRef.current?.photos ?? 0 }));
|
||||
try {
|
||||
const result = await backupNow((p) => setNote(t('lib.backupRun', { done: p.done, total: p.total })));
|
||||
await show(await backupStatus());
|
||||
if (!quiet) setNote(t('lib.backupDone', { n: result.photos, w: result.written }));
|
||||
} catch {
|
||||
if (!quiet) setNote(t('lib.backupFailed'));
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
const result = await backupNow((p) => setNote(t('lib.backupRun', { done: p.done, total: p.total })));
|
||||
await show(await backupStatus());
|
||||
if (!quiet) setNote(t('lib.backupDone', { n: result.photos, w: result.written }));
|
||||
break;
|
||||
} catch (err) {
|
||||
// What the row was reading is the permission of a moment ago, and
|
||||
// the browser is free to want the click again by the time the write
|
||||
// reaches the disk — asking for it again from here is a request
|
||||
// without one behind it, and a refusal. The picker is the only
|
||||
// thing that hands a folder back, so a refusal that names the
|
||||
// permission goes through it once; anything else is the folder
|
||||
// itself saying no, and twice is the browser's answer.
|
||||
//
|
||||
// The sentence on the screen cannot carry a reason — it is one line
|
||||
// over a strip of photographs — so the reason goes to the console
|
||||
// and a word of it into the note, which is the whole of what tells
|
||||
// a folder that was moved from a permission that lapsed.
|
||||
console.error('backup:', err);
|
||||
if (attempt === 0 && needsPick(err) && (await pickBackup())) continue;
|
||||
if (!quiet) setNote(t('lib.backupFailed', { why: why(err) }));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setBackBusy(false);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
CLARITY_SKSL,
|
||||
CLARITY_BLUR_SKSL,
|
||||
CLARITY_BLEND_SKSL,
|
||||
CLARITY_GAIN,
|
||||
DEHAZE_SKSL,
|
||||
dehazeUniformArray,
|
||||
getToneUniforms,
|
||||
@@ -1052,7 +1051,11 @@ export async function renderPhoto(input: RenderInput): Promise<RenderResult | nu
|
||||
if (!reference) return null;
|
||||
own(reference);
|
||||
const shader = clarityBlendEffect.makeShaderWithChildren(
|
||||
[(clarityKnob / 10) * (clarityKnob > 0 ? CLARITY_GAIN : 1)],
|
||||
// Amount in the blend's own [-1, 1] units: the positive side's gain
|
||||
// (CLARITY_DETAIL_GAIN) lives INSIDE the shader now, so the mask's
|
||||
// CLARITY_GAIN — a different quantity on a different child — is not
|
||||
// borrowed for this one and the two knobs stay independent.
|
||||
[clarityKnob / 10],
|
||||
[own(imageShaderChild(snap)), own(imageShaderChild(reference))]
|
||||
);
|
||||
return shader ? own(shader) : null;
|
||||
|
||||
@@ -292,7 +292,7 @@ export const en: Dict = {
|
||||
'lib.backupPick': 'Pick the folder the catalogue is written into and read back from — the folder name is the backup name.',
|
||||
'lib.backupRun': 'Backing up {done}/{total}…',
|
||||
'lib.backupDone': 'Backed up {n} frames, {w} tiles written.',
|
||||
'lib.backupFailed': 'The backup folder could not be written to.',
|
||||
'lib.backupFailed': 'The backup folder could not be written to ({why}).',
|
||||
'lib.restoreConfirm': 'Restore {n} frames from {folder}, written {time}? Frames the library already holds are left as they are.',
|
||||
'lib.restoreRun': 'Restoring {done}/{total}…',
|
||||
'lib.restoreDone': 'Restored {n} frames. Add the folder again so every frame gets its file back.',
|
||||
|
||||
@@ -305,7 +305,7 @@ export const vi = {
|
||||
'lib.backupPick': 'Chọn thư mục mà danh mục được ghi vào và đọc lại từ đó — tên thư mục chính là tên bản sao lưu.',
|
||||
'lib.backupRun': 'Đang sao lưu {done}/{total}…',
|
||||
'lib.backupDone': 'Đã sao lưu {n} ảnh, ghi {w} thumbnail.',
|
||||
'lib.backupFailed': 'Không ghi được vào thư mục sao lưu.',
|
||||
'lib.backupFailed': 'Không ghi được vào thư mục sao lưu ({why}).',
|
||||
'lib.restoreConfirm': 'Phục hồi {n} ảnh từ {folder}, ghi lúc {time}? Những ảnh thư viện đã có sẽ được giữ nguyên.',
|
||||
'lib.restoreRun': 'Đang phục hồi {done}/{total}…',
|
||||
'lib.restoreDone': 'Đã phục hồi {n} ảnh. Thêm lại thư mục ảnh để mỗi ảnh lấy lại được tệp của nó.',
|
||||
|
||||
Reference in New Issue
Block a user