3.4 KiB · text History f629a67
// The moire wallpaper's whole picture: ground, halftone dots and moire
// rings, each fading out into the ground away from a drifting hub.
//
// Everything that moves is computed in Field.qml, in double precision,
// and arrives here as positions and phases; the shader holds no clock,
// so it never loses precision however long the session runs.
//
// Compiled into ../theme/contents/ui/field.frag.qsb by tools/shaders.sh.
#version 440

layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;

layout(std140, binding = 0) uniform buf {
    mat4 qt_Matrix;
    float qt_Opacity;

    vec2 size;        // pixels
    // Positions and lengths below are in units of the height, so every
    // aspect ratio shows the same pattern at the same apparent scale.
    vec2 ringA;       // centres of the two ring families
    vec2 ringB;
    vec2 hub;         // where the pattern is strongest
    float reach;      // distance at which it has faded out entirely
    float period;     // ring spacing
    float moire;      // 0-1, how much of the rings to show
    float halftone;   // 0-1, how much of the dots to show
    float cell;       // halftone grid spacing
    float angle;      // halftone grid rotation, radians
    float wave;       // halftone wave phase, radians, kept in [0, 2pi)

    vec4 base;        // the ground
    vec4 deep;        // the ground at the edges
    vec4 ink;         // the moire
    vec4 tone;        // the halftone
};

float fade(vec2 p)
{
    float t = clamp(1.0 - length(p - hub) / reach, 0.0, 1.0);
    return t * t * (3.0 - 2.0 * t);
}

float rings(vec2 p, vec2 centre, float px)
{
    float r = length(p - centre);
    float d = abs(fract(r / period + 0.5) - 0.5) * period;
    float halfWidth = max(0.9 * px, period * 0.09);
    return clamp(1.0 - (d - halfWidth) / px, 0.0, 1.0);
}

void main()
{
    float px = 1.0 / size.y;
    vec2 p = qt_TexCoord0 * size * px;
    vec2 centre = vec2(0.5 * size.x * px, 0.5);

    // Ground: base, darkening towards deep at the corners.
    float v = smoothstep(0.35, 1.25, length(p - centre));
    vec3 colour = mix(base.rgb, deep.rgb, v);

    // Halftone: dots on a rotated grid, each sized by the fade and a
    // travelling wave sampled at its cell's centre, so a dot is round
    // and uniform rather than cut by the field. Area, not radius, is
    // proportional to the field, as in print.
    if (halftone > 0.0) {
        mat2 rot = mat2(cos(angle), sin(angle), -sin(angle), cos(angle));
        vec2 q = rot * p;
        vec2 index = floor(q / cell);
        vec2 local = (q / cell - index - 0.5) * cell;
        vec2 cellCentre = transpose(rot) * ((index + 0.5) * cell);
        float field = fade(cellCentre)
                    * (0.55 + 0.45 * sin(dot(cellCentre, vec2(0.8, 0.6)) * 9.0 - wave));
        float radius = 0.5 * cell * sqrt(field);
        float dotCover = clamp((radius - length(local)) / px + 0.5, 0.0, 1.0);
        colour = mix(colour, tone.rgb, dotCover * halftone * 0.55);
    }

    // Moire: two ring families slightly apart. Where their rings
    // interleave the light doubles up, where they coincide it thins
    // out, and that beat is the moire.
    if (moire > 0.0) {
        float a = rings(p, ringA, px);
        float b = rings(p, ringB, px);
        float light = 1.0 - (1.0 - 0.85 * a) * (1.0 - 0.85 * b);
        colour = mix(colour, ink.rgb, light * 0.6 * fade(p) * moire);
    }

    fragColor = vec4(colour, 1.0) * qt_Opacity;
}