Skip to content

Watercolor Smudge Setup + shader player image channel (#164) - #167

Merged
charliehuge merged 11 commits into
mainfrom
claude/watercolor-smudge
Nov 10, 2026
Merged

charliehuge merged 11 commits into
mainfrom
claude/watercolor-smudge

Conversation

@ii-impy

@ii-impy ii-impy commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Closes #164.

Not ready to merge until the rig test passes. The real depth-camera test on the Ultra 9 is the sign-off. The checklist is below.

Summary

There are two parts: a player change and a new Setup.

The player gains an "image" channel source (ADR-0023, which amends ADR-0021). Each load takes the next image from the Setup's images/ folder, in shuffled order:

 Rotation._load(name)
   setup = load_setup(dir / name)            # + setup.images: <setup>/images/*.png|jpg|jpeg
+  image = _next_image(setup)                # ShuffleBag per Setup; corrupt files dropped
+    decode_image(path, cover=window,        # EXIF-upright, row 0 = bottom,
+                 max_side=GL_MAX_TEXTURE)   # downscaled only while it still covers the window
   renderer.load(setup, image=image)         # unbound → reads 0, iChannelResolution 0

The new Setup, shaders/watercolor_smudge/, shows a watercolor painting. The thin, moving parts of a visitor's silhouette smear it, and it heals once the room empties:

B  original   image cover-fit, or a procedural watercolor sunset when images/ is empty
C  motion     Body softened + eased (10 Hz → smooth), last frame, thinness, empty timer
D  canvas     paint pushed along the moving silhouette edge (normal flow)
                × thinness (limbs and head yes, torso no)
                × balance gate (appearing, vanishing or flickering Body doesn't count)
              → finger streaks, pigment-style mixing, wetness
              → heals toward B: 2 s after the room empties, ~95 % in 10 s
Image         the bound image sampled directly where untouched; canvas + paper grain where smeared

All tuning values are named constants at the top of common.glsl. The design is in docs/superpowers/specs/2026-11-07-watercolor-smudge-design.md, with the approved playable sketch beside it. The build plan is in docs/superpowers/plans/2026-11-07-watercolor-smudge.md.

Also: shader_app.py can now go fullscreen on a chosen display. Use shader_app.display in captcha-settings.json, or --display N to override it. The window is sized from that display's own desktop, so a 1600×1200 projector beside a 3072×1920 main monitor gets the whole picture. Before this, the window took the main monitor's size and you only saw part of it.

Evidence

  • Before: 343 FundingCAPTCHA tests. No "image" source: load_setup raises "unknown source(s) ['image']".
    After: 377 pass (.venv/bin/python -m pytest -q in ShowControl/FundingCAPTCHA/). The new behaviour tests render through real GL:
    • test_still_flickering_person_leaves_paint_alone: a still limb with 1-camera-px rim flicker and edge dropout at 10 Hz → max canvas change < 0.02
    • test_moving_limb_smears_most_of_its_path → over 30 % of the path smeared. It was 7 % before the softening fix that came out of the look check.
    • test_moving_torso_does_not_smear → < 0.03
    • test_heal_waits_for_delay_then_heals_in_heal_time, test_heal_pauses_when_someone_returns
    • test_image_covers_screen_keeping_aspect, test_image_stays_sharp_outside_smear, test_black_paint_at_centre_still_smears
    • test_rotation_shuffles_images_per_load, test_rotation_skips_corrupt_image, test_rotation_with_only_corrupt_images_binds_nothing, test_decode_image_applies_exif_orientation
  • Look check, rendered headlessly at 800×450 with a walking, waving stick figure: walking smudges the water, the wave drags the sun into the cloud, and after everyone leaves it heals clean, which matches the approved sketch. A temporary portrait image also checked out: it cover-fits, stays sharp where untouched, smears, and heals. It wasn't committed.

Known limitation, pinned by test_reaching_arm_along_its_axis_does_not_smear_yet:

  • What happens: an arm reaching straight out along its own length doesn't smear. It only gains Body at the tip, so the balance gate reads it as appearing. Waving, swinging and walking do smear.
  • What was tried: two bounded fixes, each of which broke the torso or flicker rejection.
  • What to try at the rig: raise BALANCE_LO/BALANCE_HI if reaching matters more than flicker.

Rig checklist: run python3 shader_app.py --camera --setup watercolor_smudge, and press R to reload after edits.

  • Stand still for 30 s: no smear.
  • Walk through, wave, and reach.
  • Leave and watch the heal.
  • Note the fps log line.

To tune, adjust these constants:

Symptom Constant
Smear too broad THIN, RING_R1/RING_R2
Too weak SMEAR_STRENGTH, BODY_SOFT
A still person smears MIN_FLOW, BODY_EASE_S
Heal speed HEAL_TIME_S

Merge Danger

Door: two-way. The new code is additive: a new channel source, a new Setup folder, and one line added to setups.json. Reverting removes it cleanly.

Blast Radius: rotation.

  • watercolor_smudge joins the shader rotation for every event that runs shader_app.py.
  • Rotation._load now decodes images for any Setup that binds "image". No existing Setup does, so their behaviour is unchanged.
  • A large image adds a decode hitch at the change to this Setup.

Deferred minors from the final review:

  • After a corrupt file is skipped, a just-shown image can repeat.
  • A broken edit followed by R still uses up an image.
  • A failed image upload can leave the renderer half-loaded.
  • The comment on PRESENT_HEAL overstates what it does in 16-bit.
  • T_CLAMP is unused.
  • A variable named step shadows the GLSL built-in.
  • The flicker test would be stricter on a checker image.

🤖 Generated with Claude Code

ii-impy and others added 9 commits November 7, 2026 10:58
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…164)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oom empties (#164)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e sketch (#164)

Look check against the approved sketch found the smear far weaker: a
walking limb jumps further between 10 Hz camera frames than its own
width, so easing cross-faded it rather than sliding it. Soften the Body
(BODY_SOFT), measure extremity on the same eased Body, and retune the
flow regulariser and dead zone. New test pins smear coverage of a path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Canvas paint floors at 1/512 so black paint at the centre isn't
  mistaken for a cleared buffer (which reset the canvas every frame).
- Images keep enough pixels to cover the window, not fit inside it,
  since the Setup cover-fits them; capped at GL_MAX_TEXTURE_SIZE.
- Images are turned upright per EXIF orientation.
- Pin the known limitation that an arm reaching along its own axis
  doesn't smear (the balance gate reads it as appearing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…isplay N)

Fullscreen sized the window from the first display's desktop, so on a
second monitor with a different resolution only part of the picture
showed. Size it from the chosen display's own desktop and open there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…timate (#164)

FLOW_EPS 4.0 dominated |∇B|² in v = −dB/dt · ∇B / (|∇B|² + eps), so limb
speed was underestimated several-fold and smears read as faint on camera.
Measured on the test sweep: path coverage ~33 % → ~70–75 %.

- FLOW_EPS 4.0 → 0.25, SMEAR_STRENGTH 0.63 → 1.5, MIX_MOVE 0.35 → 0.5
- Wet trail more visible: WET_DARKEN 0.07 → 0.15, WET_DECAY 0.35 → 0.2

Flicker and moving-torso guards still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: shader_app.py (main split it into open_display / ShaderStage /
start_camera for the playlist player), shaders/setups.json, ADR-0021.

- --display / shader_app.display now go through the shared open_display.
- The per-Setup image bags move out of Rotation into SetupImages so the
  playlist player can share them.
- The image-channel ADR becomes ADR-0024: main took 0023 for the
  self-learning depth background.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EiZFhwnAeRgXxtUydwrLsQ
- playlist_app.Playlist hands each Setup load the next image from its bag,
  as shader_app.Rotation does, so watercolor_smudge paints over its
  pictures in the show player too. Added to playlist.json.
- playlist_app honours --display / shader_app.display.
- requirements-dev.txt gains Pillow: the image-decode tests failed in CI
  with ModuleNotFoundError.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EiZFhwnAeRgXxtUydwrLsQ
@charliehuge
charliehuge merged commit b0dee63 into main Nov 10, 2026
1 check passed
@charliehuge
charliehuge deleted the claude/watercolor-smudge branch November 10, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FundingCAPTCHA Setup: Watercolor Smudge — silhouettes smear a painted background

3 participants