/*
 * The emulsion, as a layer over the picture.
 *
 * THIS FILE IS LOADED BY BOTH PAGES, and that is what keeps the workbench honest:
 * the grain you tune at /dev/drawing is the grain the live site shows, because it is
 * the same stylesheet over the same image. If it lived in only one of them the two
 * would drift, and the workbench would stop being a preview of anything.
 *
 * WHY IT IS NOT IN THE VIDEO. Grain is new random noise in every frame — the one
 * thing a video codec cannot predict from the frame before, which is the whole basis
 * of how video compression works. With it baked in, the three recordings came to
 * 113 MB and the encoder was still spending its entire budget on noise rather than
 * on the line. Over the top it costs about a kilobyte and looks better, because it
 * is generated at the viewer's own device resolution instead of being upscaled out
 * of a video: grain stays the size grain should be on a retina screen rather than
 * growing into blotches.
 *
 * WHY feTurbulence AND NOT A PNG. Four tiles of real gaussian noise are four files
 * of pure entropy — about 260 kB, because noise is the thing PNG cannot compress
 * either. These are four data URIs of a few hundred bytes each: the browser
 * generates the noise itself from a seed. Not gaussian, and it does not have to be —
 * `fractalNoise` through a desaturating matrix is what grain looks like, and the
 * canvas renderer's Box-Muller tiles are still there in film.js for anyone who wants
 * to compare (`film.grain.mode: 'canvas'` in config.js).
 *
 * FOUR SEEDS CYCLED, NOT ONE PATTERN MOVED. A translated pattern is the same
 * pattern, and the eye catches it sliding.
 *
 * 24Hz, because film runs at 24. Grain that changes every frame is television
 * static; grain that changes 24 times a second is film. `steps(4)` over 1/6th of a
 * second is four changes in 166ms — 24 a second.
 */

.grain {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /*
   * OVERLAY, not multiply or screen. Grain does not darken the picture or lighten
   * it; it pushes every pixel a little to either side of where it was, which is what
   * a random exposure variation in an emulsion does. multiply would fog the ground
   * and screen would wash it out.
   */
  mix-blend-mode: overlay;
  /*
   * MEASURED AGAINST THE THING IT REPLACES, not chosen by eye.
   *
   * film.js's canvas grain is gaussian at sigma 11.55, and through `overlay` onto a
   * mid-tone ground that lands as about 10.8 levels of standard deviation in the
   * finished picture. This layer was read back off a rendered page at several
   * opacities — 0.60 gave 8.2, 0.78 gave 10.5, 0.90 gave 12.4 — so 0.78 is the value
   * at which swapping the grain out of the renderer and onto the page changes the
   * amount of grain by nothing anyone could see. Tone shift at that setting is under
   * one level in 255.
   */
  opacity: 0.78;
  background-repeat: repeat;
  /* Fixed in CSS pixels, so the grain is the same size on every screen and does not
     grow with the picture. */
  background-size: 180px 180px;
  animation: grain-cycle 0.1666s steps(1) infinite;
  will-change: background-image;
}

/*
 * Four turbulence seeds.
 *
 * `baseFrequency` is high — this is grain, not cloud — and `numOctaves` 1 keeps it a
 * single fine scale rather than a cloudy fractal. `stitchTiles` makes the tile
 * seamless, which matters because the layer is repeated rather than stretched.
 *
 * THE feColorMatrix IS DOING TWO JOBS AND THE SECOND ONE IS NOT OPTIONAL. The first
 * three rows average RGB into grey, because silver halide grain is monochrome and
 * per-channel colour noise is a digital sensor artefact. The last row forces alpha
 * opaque — feTurbulence writes a random ALPHA channel as well, and left alone the
 * layer is mostly transparent noise with almost nothing in it to blend. That is
 * exactly how this looked on the first attempt: a grain layer that was present in
 * the DOM, cost a repaint, and could not be seen.
 *
 * color-interpolation-filters='sRGB' IS LOAD-BEARING, and its absence is the second
 * trap in the same eight lines. An SVG filter primitive works in linearRGB by
 * default, so feTurbulence's mid-grey — the value `overlay` treats as "leave the
 * backdrop alone" — comes out at about 0.735 once it is encoded to sRGB. The grain
 * then does not sit symmetrically about the picture, it LIFTS it: the second attempt
 * put visible grain on the page and washed a full stop of exposure out of the
 * material at the same time. One attribute, and the layer is neutral again.
 */
@keyframes grain-cycle {
  0% {
    background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='180'%20height='180'%3E%3Cfilter%20id='g'%20x='0'%20y='0'%20width='100%25'%20height='100%25'%20color-interpolation-filters='sRGB'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.85'%20numOctaves='1'%20seed='1'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='matrix'%20values='0.33%200.33%200.33%200%200%20%200.33%200.33%200.33%200%200%20%200.33%200.33%200.33%200%200%20%200%200%200%200%201'/%3E%3C/filter%3E%3Crect%20width='180'%20height='180'%20filter='url%28%23g%29'/%3E%3C/svg%3E");
  }
  25% {
    background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='180'%20height='180'%3E%3Cfilter%20id='g'%20x='0'%20y='0'%20width='100%25'%20height='100%25'%20color-interpolation-filters='sRGB'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.85'%20numOctaves='1'%20seed='7'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='matrix'%20values='0.33%200.33%200.33%200%200%20%200.33%200.33%200.33%200%200%20%200.33%200.33%200.33%200%200%20%200%200%200%200%201'/%3E%3C/filter%3E%3Crect%20width='180'%20height='180'%20filter='url%28%23g%29'/%3E%3C/svg%3E");
  }
  50% {
    background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='180'%20height='180'%3E%3Cfilter%20id='g'%20x='0'%20y='0'%20width='100%25'%20height='100%25'%20color-interpolation-filters='sRGB'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.85'%20numOctaves='1'%20seed='19'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='matrix'%20values='0.33%200.33%200.33%200%200%20%200.33%200.33%200.33%200%200%20%200.33%200.33%200.33%200%200%20%200%200%200%200%201'/%3E%3C/filter%3E%3Crect%20width='180'%20height='180'%20filter='url%28%23g%29'/%3E%3C/svg%3E");
  }
  75% {
    background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='180'%20height='180'%3E%3Cfilter%20id='g'%20x='0'%20y='0'%20width='100%25'%20height='100%25'%20color-interpolation-filters='sRGB'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.85'%20numOctaves='1'%20seed='31'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='matrix'%20values='0.33%200.33%200.33%200%200%20%200.33%200.33%200.33%200%200%20%200.33%200.33%200.33%200%200%20%200%200%200%200%201'/%3E%3C/filter%3E%3Crect%20width='180'%20height='180'%20filter='url%28%23g%29'/%3E%3C/svg%3E");
  }
}

/*
 * Reduced motion: the grain stops moving but does not go away. A still grain is
 * still a photograph of a material; no grain is a flat digital surface, which is a
 * different picture. Only the flicker was ever the problem.
 */
@media (prefers-reduced-motion: reduce) {
  .grain { animation: none; }
}
