Soft Shoulder / how dense ink keeps its gradation

Additive ink is unbounded: overlapping strokes stack well past 1.0, and a raw write clips every saturated channel to one flat white. The shoulder compresses overshoot through an exponential roll-off instead — byte-identical below the knee, slope 1 there, asymptote just under paper-white. Left of the divider, the raw clip. Right, the shoulder. Paint across the boundary.

raw · clamp soft shoulder drag to inject ink · divider drags · ←/→ move it

SOFT SHOULDER — UNSUPPORTED

Probing…

transfer · input → outputper channel
accumulation is real: values stack unbounded in a half-float target

The test cell accumulates gaussian splats additively into an RGBA16F target — the exact failure shape that produced flat-white plateaus in Dye Bench's dye view. Nothing about the accumulation differs left from right; only the present pass does. That is the point: this is a display-side fix, and the solver never knows it happened.

Drop-in · executed live above
  1. An unbounded accumulation target — half-float renderable (WebGL2 + EXT_color_buffer_float here) whose values legitimately exceed 1.0 before display.
  2. To run on the present pass only. Data textures, solver fields and readbacks are untouched; the shoulder lives between simulation and screen.
  3. SHOULDER_GLSL — the chunk, drop-in ready; shoulder(c), a JS reference implementation matching it bit-for-bit in spirit for plotting/tests; createShoulderCell(canvas), the whole accumulate-and-present cell behind the demo.
  4. Byte-identical below the knee (0.72): inputs under it pass through unchanged, so ordinary tones cannot shift.
  5. C¹ continuity at the knee, slope 1: no visible seam where compression begins; the derivative matches on both sides.
  6. Asymptote 0.985 (knee 0.72 + shoulder 0.265): output approaches paper-white but never reports a false 1.0 — dense cores keep gradation and hue because the map is monotone per channel.
  7. The solver stays honest: this fixes presentation, never data. Any HUD quantity is read from the untouched field, not the toned image.
  8. Fails by naming, like every GPU page here: without WebGL2 or float render targets the cell renders a card that says which capability is missing.
  9. Seeded stillness: the opening composition comes from mulberry32(20260822) and nothing animates unattended — screenshots and reduced motion get the same frame.
  10. Dye Bench (designpages.dev/dye-bench), dye-view fragment: the KNEE/SHOULDER constants and the min(col,KNEE) + S·(1−exp(−over/S)) expression, plus the reasoning comment that justified them.