Give a gradient the angle it was asked for
A linear mask at 45° was not at 45°, and a radial with equal radii was an ellipse. Both on every photograph that is not square, which is all of them. The geometry is stored in normalised coordinates so that a mask survives a crop, a zoom and an export at another size — that part was right. What was wrong is that a *distance* was being measured in those coordinates too, and a fraction of the width is not the same length as a fraction of the height. So `dot(uv - centre, axis)` measured the ramp in a space one of whose axes is squashed against the other by the aspect ratio, and the iso-lines came out sheared: on a 3:2 frame a ramp asked for at 45° arrives at about 34°. Nothing announces it. The stored numbers are exactly what was written, the shader is doing exactly what it says, and the only place the fault exists is between the photographer's intent and the picture. It has been invisible so far because there is no way yet to place a gradient by eye — the handles that make it visible are what turned it up. So distances and angles move into the frame's own isotropic units: y spans `0..1` and x spans `0..aspect`, which makes a circle round and 45° a real diagonal. The centre stays a plain fraction of each axis, because it is a point and a point has no such problem — and because that is the space a click arrives in. `frame_delta` is the one conversion and must stay the only one; the mask array's own dimensions carry the aspect, so it costs no uniform. The sidecar format does not change. What changes is what the numbers mean, and the only geometry in the wild is a default that has never been movable. The two tests are at 96×64 rather than square, which is the whole point: on a square target this bug cannot be reproduced, and every existing mask test was square. Both fail without the conversion — the radial reaching 28px sideways where it reaches 19px down, and the diagonal landing on the wrong side of the line it is supposed to lie along. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,9 @@ struct MaskParams {
|
||||
// `falloff_code` on the Rust side.
|
||||
falloff: u32,
|
||||
|
||||
// Geometry, in normalised output coordinates. Meaning depends on `mode`.
|
||||
// Geometry. Meaning depends on `mode`. The centre is in normalised 0..1
|
||||
// coordinates; every distance below it is in the isotropic frame units
|
||||
// `frame_delta` establishes.
|
||||
centre: vec2<f32>,
|
||||
// Linear: (cos, sin) of the ramp direction. Radial: semi-axes.
|
||||
axis: vec2<f32>,
|
||||
@@ -124,9 +126,23 @@ fn region_mask(px: vec2<i32>) -> f32 {
|
||||
return total / n;
|
||||
}
|
||||
|
||||
// Offset from a gradient's centre, in the frame's own **isotropic** units:
|
||||
// y spans 0..1 and x spans 0..aspect, so a step of the same length means the
|
||||
// same distance whichever way it points.
|
||||
//
|
||||
// Without this the geometry lives in raw 0..1, where one axis is compressed
|
||||
// against the other by the aspect ratio — so a 45° ramp is not at 45° on
|
||||
// anything but a square frame, and a radial with equal radii draws an ellipse.
|
||||
// Both faults are invisible in the stored numbers and obvious the moment a
|
||||
// handle is dragged on a photograph, which is what this exists for.
|
||||
fn frame_delta(uv: vec2<f32>) -> vec2<f32> {
|
||||
let aspect = vec2<f32>(f32(p.width) / f32(max(p.height, 1u)), 1.0);
|
||||
return (uv - p.centre) * aspect;
|
||||
}
|
||||
|
||||
fn linear_mask(uv: vec2<f32>) -> f32 {
|
||||
// Signed distance along the ramp direction, from the centre.
|
||||
let d = dot(uv - p.centre, p.axis);
|
||||
let d = dot(frame_delta(uv), p.axis);
|
||||
if (p.softness <= 0.0) {
|
||||
return select(0.0, 1.0, d >= 0.0);
|
||||
}
|
||||
@@ -136,7 +152,7 @@ fn linear_mask(uv: vec2<f32>) -> f32 {
|
||||
fn radial_mask(uv: vec2<f32>) -> f32 {
|
||||
let ca = cos(-p.angle);
|
||||
let sa = sin(-p.angle);
|
||||
let d = uv - p.centre;
|
||||
let d = frame_delta(uv);
|
||||
// Into the ellipse's own frame, then normalised by its semi-axes so the
|
||||
// problem becomes a unit circle.
|
||||
let local = vec2<f32>(d.x * ca - d.y * sa, d.x * sa + d.y * ca);
|
||||
|
||||
Reference in New Issue
Block a user