diff --git a/docker/frontend/src/styles/app.css b/docker/frontend/src/styles/app.css index 0742e84..5926267 100644 --- a/docker/frontend/src/styles/app.css +++ b/docker/frontend/src/styles/app.css @@ -744,7 +744,12 @@ button.hint:disabled { cursor: default; } wash turns into an opaque stripe over a warm one. A stroke is one path, so where it crosses itself its translucency is laid once rather than twice — that doubling-up is exactly what a row of overlapping - circles looks like. */ + circles looks like. + The wash is a mark over the photo, though, and over a smooth sky a repair + reads as a colour of its own that the photo does not have — the one thing a + repair must never look like. So it is worn only while the pointer is on that + run (the mark names the repair in hand) and the photo is left bare of it the + rest of the time. */ .brush-band { position: absolute; inset: 0; @@ -760,22 +765,20 @@ button.hint:disabled { cursor: default; } stroke-linecap: round; stroke-linejoin: round; } +/* No run in hand here: the band is the tool's record, not the photo's own + colour, so it steps out of the way of what it was drawn over. */ +.brush-band path[data-idle] { opacity: 0; } /* A spot the band stands for: its box on the photo is still the spot's, its own edge is the band's now. */ .brush-spot[data-band] { border-color: transparent; box-shadow: none; } /* A repair the pointer is not on and no press has chosen: the ring would be one more circle on a photo that already carries as many repairs as the user laid, - so an idle spot is a faint print of the mark it made — soft, because it is a - record of an edit rather than something to aim at, and it is the ring under - the pointer that says the spot can be taken hold of again. */ -.brush-spot[data-idle] { - border-color: transparent; - background: rgba(127, 127, 127, 0.14); - box-shadow: 0 0 7px 2px rgba(0, 0, 0, 0.25); - filter: blur(1px); -} -/* A run the band already stands for needs no print of its own under it. */ -.brush-spot[data-idle][data-band] { background: none; box-shadow: none; filter: none; } + and a print of the mark it made is a wash of the tool's own over the photo — + over a smooth sky, a repair that reads as a colour the photo does not have. + So an idle spot shows nothing at all: it is the ring under the pointer that + says the spot can be taken hold of again, and the photo carries only the + repair itself until then. */ +.brush-spot[data-idle] { opacity: 0; } /* In hand, one of the run's spots is the circle it would be taken hold of by, so it wears that circle again over the band — the ring is what says the spot is under the pointer. */ diff --git a/docker/frontend/src/ui/ImageStage.tsx b/docker/frontend/src/ui/ImageStage.tsx index 13a2eea..3199b48 100644 --- a/docker/frontend/src/ui/ImageStage.tsx +++ b/docker/frontend/src/ui/ImageStage.tsx @@ -90,14 +90,16 @@ const wmBoxOf = (mark: WmMark, fontPx: number) => { // after a reload, an undo, or a copy of the recipe to another photo: a stroke // is not stored anywhere. const strokeRuns = (spots: { x: number; y: number; r: number }[], box: { width: number; height: number }) => { - const paths: { d: string; w: number }[] = []; + const paths: { d: string; w: number; spots: number[] }[] = []; const joined = spots.map(() => false); let run: number[] = []; const flush = () => { if (run.length > 1) { const w = 2 * spots[run[0]].r * box.width; const at = (i: number) => `${spots[i].x * box.width} ${spots[i].y * box.height}`; - paths.push({ d: `M ${run.map(at).join(' L ')}`, w }); + // The run's own spots travel with the band, so the band can be shown for a + // run the pointer is on and left off for the runs it is not. + paths.push({ d: `M ${run.map(at).join(' L ')}`, w, spots: run.slice() }); for (const i of run) joined[i] = true; } run = []; @@ -703,6 +705,22 @@ export function ImageStage({ })() : null; + // Whether a run's own band is the one in hand: the pointer is on one of the + // spots the run is made of, or a press has chosen one of them. A band is the + // tool's mark over the photo and not the photo, so every other run's band — + // and every other repair's print — stays off it, and what a repair looks like + // is what the renderer made of it. + const bandOn = (b: { spots: number[] }) => { + if (selSpot !== null && b.spots.includes(selSpot)) return true; + if (!brushPos || !box) return false; + return b.spots.some((i) => { + const s = brushSpots[i]; + return ( + !!s && Math.hypot((brushPos.fx - s.x) * box.width, (brushPos.fy - s.y) * box.height) <= Math.max(4, s.r * box.width) + ); + }); + }; + const brushDown = (e: React.PointerEvent) => { // The stage's own hand, under the brush: the middle button and the space // bar are how the photo is moved out of the way when the brush is down, and @@ -1637,7 +1655,12 @@ export function ImageStage({ run is made of, joined into the one mark the drag painted. What is drawn is the recipe's own spots, so the band is what survives a reload, an undo and a copy of the recipe to another - photo, where a stroke of its own would not. */} + photo, where a stroke of its own would not. It, like the print + of a spot, is the tool's own record and not part of the photo: + it is worn while the pointer is on that run — the mark says + which repair is in hand — and given up again the moment the + pointer leaves, so what a repair left on the photo is read off + the photo and not off a wash laid over it. */} {bands?.dst.paths.length ? ( ) : null}