precision highp float;

/** @resolution */
uniform vec2 u_resolution;

/** @time */
uniform float u_time;

/**
 * @label Ink
 * @color
 * @default #ffffff
 */
uniform vec3 u_ink;

// Requires derivatives (fwidth) to width the strokes in pixels.
//
// WebGL1 ONLY, with OES_standard_derivatives, which the host enables and
// prepends the #extension directive for. This is GLSL ES 1.00 and WebGL2 gives
// derivatives only to ES 3.00, so on a WebGL2 context getExtension returns null
// AND the directive is rejected. See ShaderHero.tsx.

// ---------------------------------------------------------------------------
// The artwork, exactly.
//
// designs/genart-hero-example.svg is 42 single-segment cubic Beziers, and they
// are an Illustrator BLEND: every control point steps by a constant vector, so
// curve i is the linear interpolation of the first and last curve at i/41.
// Verified against the SVG at a maximum control-point error of 1.35px, which is
// just Illustrator rounding its output.
//
// That is why this shader can reproduce the reference rather than approximate
// it. An earlier version modelled the family as one edge curve plus a sqrt fan,
// which assumes every line is a vertical translate of that edge. Measuring the
// reference showed the gaps between lines vary by a factor of ~18 across the
// frame, so the assumption was wrong and no tuning of the edge could fix it.
// The blend model lands 4x closer to the measured line positions.
// ---------------------------------------------------------------------------
const vec2 REF = vec2(1440.0, 641.0);

const vec2 A0 = vec2(-584.23,  326.78);
const vec2 A1 = vec2( 271.11, 1333.88);
const vec2 A2 = vec2(1126.44,   80.03);
const vec2 A3 = vec2(1981.78, 1076.05);

const vec2 B0 = vec2(  -3.25,  328.81);
const vec2 B1 = vec2( 468.02,  600.64);
const vec2 B2 = vec2( 939.30, -367.16);
const vec2 B3 = vec2(1410.58,  -89.22);

const float LAST = 41.0;          // 42 curves, indices 0..41

// Stroke greys from the SVG: #959595 on the backmost curve ramping to #040404
// on the frontmost. Emitting these rather than flat white is what makes the
// result composite over the page background identically to the static PNG.
const float GREY_BACK  = 0.58431;  // 149/255
const float GREY_FRONT = 0.01569;  //   4/255

// Recession, in curve indices per second.
//
// The lines slide THROUGH the blend, so every frame is still exactly on the
// reference ruled surface and the shape can never go wrong. Only which lines
// are lit changes.
//
// Direction: index 0 is the widely spaced end at the bottom of the frame and
// index 41 is where they pack against the crest, so the family already has the
// spacing gradient of a ground plane running to a horizon. Advancing s with
// time therefore carries each line from the near end toward the packed end,
// and because the gaps compress on the way, a line visibly slows as it goes.
// That deceleration is what sells the distance; a constant on-screen speed
// would read as a flat scroll.
//
// At 0.6 a line takes about 68 seconds to cross the whole fan.
const float DRIFT = 0.6;

// Cost control. The bracket is 1/SCAN wide and each refine step halves it, so
// the final resolution in t is 1/(SCAN * 2^REFINE). At 40 and 12 that is 6e-6,
// where about 4e-5 is needed to place a line within a tenth of a pixel, so
// there is margin. The scan also stops as soon as it brackets, which is most of
// the saving: only the empty sky above the ribbon pays for all SCAN steps.
const int  SCAN   = 40;           // coarse samples used to bracket the root
const int  REFINE = 12;           // bisection steps inside that bracket

vec2 bezA(float t){
  float m = 1.0 - t;
  return m*m*m*A0 + 3.0*m*m*t*A1 + 3.0*m*t*t*A2 + t*t*t*A3;
}
vec2 bezB(float t){
  float m = 1.0 - t;
  return m*m*m*B0 + 3.0*m*m*t*B1 + 3.0*m*t*t*B2 + t*t*t*B3;
}

void main(){
  // Reproduce the CSS the static layer uses: `cover`, positioned `50% 0%`. The
  // hero box is a different aspect from the artwork, so the browser scales to
  // cover and crops the sides. Doing the same here is what keeps the two in
  // register at every viewport size. A previous version tried to compensate
  // with a single aspect ratio constant, which only agreed at one box shape.
  float scale = max(u_resolution.x / REF.x, u_resolution.y / REF.y);
  float offX  = (u_resolution.x - REF.x * scale) * 0.5;
  vec2  screen = vec2(gl_FragCoord.x, u_resolution.y - gl_FragCoord.y);
  vec2  q = vec2((screen.x - offX) / scale, screen.y / scale);

  // Because the blend is linear in the curve index, the family is a ruled
  // surface: for a fixed t the points sweep a straight segment from curve A to
  // curve B. So a pixel is located by finding the t whose segment passes
  // through it, which is a single root find, and the position along that
  // segment is the curve index. Measured over the frame that root is unique
  // wherever the ribbon is drawn, so bracketing it is safe.
  float lo = 0.0, hi = 0.0, flo = 0.0;
  bool  bracketed = false;

  vec2  pa = bezA(0.0);
  vec2  pd = bezB(0.0) - pa;
  vec2  r  = q - pa;
  float fPrev = r.x * pd.y - r.y * pd.x;
  float sPrev = dot(r, pd) / dot(pd, pd);
  float tPrev = 0.0;

  for (int i = 1; i <= SCAN; i++){
    float t = float(i) / float(SCAN);
    pa = bezA(t);
    pd = bezB(t) - pa;
    r  = q - pa;
    float f = r.x * pd.y - r.y * pd.x;
    float s = dot(r, pd) / dot(pd, pd);

    // Take the first sign change whose segment position lands inside the band.
    // Outside the ribbon there are other roots, and they all sit outside [0,1].
    if (!bracketed && fPrev * f < 0.0 && max(sPrev, s) >= 0.0 && min(sPrev, s) <= 1.0){
      lo = tPrev; hi = t; flo = fPrev; bracketed = true;
      break;
    }
    tPrev = t; fPrev = f; sPrev = s;
  }

  if (!bracketed) discard;

  for (int k = 0; k < REFINE; k++){
    float mid = 0.5 * (lo + hi);
    pa = bezA(mid);
    pd = bezB(mid) - pa;
    r  = q - pa;
    float f = r.x * pd.y - r.y * pd.x;
    if (f * flo > 0.0){ lo = mid; flo = f; } else { hi = mid; }
  }

  float t = 0.5 * (lo + hi);
  pa = bezA(t);
  pd = bezB(t) - pa;
  r  = q - pa;
  float s = dot(r, pd) / dot(pd, pd);
  // A small margin outside the band: a pixel sitting just beyond the first or
  // last curve is still within half a stroke of it, and clipping at exactly
  // [0,1] sliced those two strokes down the middle and left the leading edge
  // of the ribbon drawn as a dotted line.
  if (s < -0.05 || s > 1.05) discard;

  // Minus, so a given line travels toward INCREASING s: the locus of line n is
  // s = (n + DRIFT*u_time) / LAST, which runs from the widely spaced near end
  // toward the packed crest. Flipping this sign makes the lines advance on the
  // viewer instead.
  float idx = s * LAST - u_time * DRIFT;

  // Nearest line, with no clamp on the index. The band is a fixed window on s,
  // so as the drift shifts idx that window still holds about 42 lines: they
  // enter at the near end and leave at the packed one. Clamping to 0..41 here
  // would instead pin the outermost two lines in place and let the rest slide
  // out from under them.
  float n = floor(idx + 0.5);

  // Width the stroke in PIXELS. The gap between lines varies enormously across
  // the frame, so a width fixed in index units would be fat where they are far
  // apart and vanish where they crowd. The artwork strokes are 1 unit wide in
  // artwork space, which is `scale` device pixels here.
  float w = max(fwidth(idx), 1e-5);
  float distPx = abs(idx - n) / w;
  // 0.436 rather than the nominal 0.5 half-width: measured against the
  // reference render, a nominal 1-unit stroke lays down 14.7% more ink than the
  // artwork does, because the vector renderer antialiases the hairlines thinner
  // than their nominal width. This lands total ink mass within 1% of it.
  float halfW  = 0.436 * scale;
  float cov = clamp(halfW + 0.5 - distPx, 0.0, 1.0);

  // Where the family packs tighter than the pixel grid the lines cannot be
  // resolved; fade them rather than drawing aliased fragments. Under motion
  // this doubles as the far-distance haze: a receding line dissolves into it
  // instead of stopping dead at the crest.
  cov *= 1.0 - smoothstep(0.35, 0.85, w);

  // Fade across both edges of the band so lines emerge and retire rather than
  // popping on at the boundary curves. Without drift these two would be hard
  // edges, which is what the artwork itself draws.
  cov *= smoothstep(-0.02, 0.06, s) * (1.0 - smoothstep(0.94, 1.02, s));

  if (cov <= 0.0) discard;

  float grey = mix(GREY_BACK, GREY_FRONT, clamp(s, 0.0, 1.0));
  gl_FragColor = vec4(u_ink * grey, cov);
}
