darkly 0.5.0

A GPU-native paint engine on wgpu: brushes, layers, blend modes, masks, selections, and undo.
Documentation
// Multi-tap soft downscale used by the veil chain when a veil renders at
// reduced resolution. Replaces the single-tap bilinear blit, which acts as
// a fixed 2×2 box filter regardless of the downscale ratio and aliases
// hard for filters (like Painting) whose output is hypersensitive to
// small input differences.
//
// Each output pixel takes 4 bilinear taps positioned at the corners of
// its footprint in the input texture. The footprint size is derived from
// screen-space derivatives — `dpdx(uv)` is the change in input UV per
// output pixel in X, i.e. exactly one output pixel's width in input-UV
// space — so the shader self-adapts to any source/destination ratio
// without needing a uniform.

struct VertexOutput {
    @builtin(position) position: vec4f,
    @location(0) uv: vec2f,
}

@vertex fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
    var out: VertexOutput;
    let uv = vec2f(f32((idx << 1u) & 2u), f32(idx & 2u));
    out.position = vec4f(uv * 2.0 - 1.0, 0.0, 1.0);
    out.uv = vec2f(uv.x, 1.0 - uv.y);
    return out;
}

@group(0) @binding(0) var t_input: texture_2d<f32>;
@group(0) @binding(1) var t_sampler: sampler;

@fragment fn fs_downscale(in: VertexOutput) -> @location(0) vec4f {
    // Output pixel size in input-UV space. We take its absolute value
    // because the vertex shader flips V, so dpdy may be negative.
    let footprint = vec2f(abs(dpdx(in.uv.x)), abs(dpdy(in.uv.y)));

    // 4 bilinear taps at the centers of the 4 quadrants of the output
    // pixel's input footprint — i.e. ±¼ of the footprint from center
    // along each axis. At exactly 2× downscale this lands each tap at a
    // half-texel position so bilinear filtering averages 2 input texels
    // and the 4 taps tile the 2×2 input area exactly. At lighter
    // downscales (e.g. 1.4× for scale=0.7) the taps fall closer
    // together and we still get a clean weighted box.
    let tap = footprint * 0.25;

    let s00 = textureSample(t_input, t_sampler, in.uv + vec2f(-tap.x, -tap.y));
    let s10 = textureSample(t_input, t_sampler, in.uv + vec2f( tap.x, -tap.y));
    let s01 = textureSample(t_input, t_sampler, in.uv + vec2f(-tap.x,  tap.y));
    let s11 = textureSample(t_input, t_sampler, in.uv + vec2f( tap.x,  tap.y));
    return 0.25 * (s00 + s10 + s01 + s11);
}