Skip to content

Screenshot pixel diff: first-class view with slider/overlay #127

@gregoryfoster

Description

@gregoryfoster

Background

Split out from the descoped #115 Phase D. Phase A + B.1 of the diff viewer have shipped; this is the remaining "promote screenshot comparison to a first-class view" item.

Current state

  • The Watch pipeline already captures Playwright screenshots (src/core/screenshot.py) and computes a Change.visual_change_score (perceptual similarity).
  • The Change Detail page renders a side-by-side "Visual Comparison" block with the two raw screenshots (pages/change_detail.html near line 49) when both snapshots have screenshot_path set.
  • That block is informational — there's no overlay, no slider, no diff highlighting. Users see "Before" and "After" but have to eyeball the differences themselves, which is impractical for large pages.

Goal

Promote the Visual Comparison from "two thumbnails" to a real visual diff tool:

  1. Slider mode — single image area with a draggable vertical handle; left of handle shows Before, right shows After. Standard before/after slider pattern.
  2. Overlay mode — Before + After stacked, with a per-pixel difference mask highlighted in magenta or similar high-contrast color. Useful for spotting subtle changes a slider misses.
  3. Mode toggle — a segment-group like the existing diff modes: Side-by-side (current), Slider, Overlay.
  4. Optional: zoom/pan — for high-resolution screenshots, click-to-zoom.

Approach sketch

  • Pixel-diff computation: Pillow already pulled in (or via pillow extra). Use ImageChops.difference + a threshold to produce an alpha-masked overlay PNG. Compute server-side once, cache to storage alongside the snapshots; serve via a new route /watches/{id}/screenshot-diff?from={prev_id}&to={curr_id}.
  • Slider (client-side): vanilla JS + CSS clip-path on the After image, wired to a draggable handle. ~50 lines, no JS dep.
  • Overlay (client-side): just an <img> with the precomputed diff PNG layered over the After.
  • Mode switching: Alpine or plain JS toggling visibility of the three modes.

Acceptance

  • Three-segment toggle: Side-by-side / Slider / Overlay
  • Slider: smooth drag; touch + keyboard support (a11y — left/right arrow keys when slider has focus)
  • Overlay: changed pixels visibly marked; aria-label describes what's shown
  • Pixel-diff PNG is computed once per Change, cached, served via a route
  • Dark mode: slider handle and overlay legend remain readable
  • Graceful fallback: if either snapshot lacks screenshot_path, the toggle is hidden (current behavior)
  • No regression in the existing side-by-side rendering

Open design questions

  1. Where does the diff PNG live? LocalStorage under a new screenshots-diff/{change_id}.png path, computed lazily on first view? Or eagerly during the check pipeline?
  2. Threshold tuning — anti-aliasing differences shouldn't show; need a perceptual threshold, not bitwise.
  3. Long pages — Playwright captures full-page screenshots; some watches produce 10000+ px tall images. Slider/overlay UX needs to handle that (scroll within the visual area).

Dependencies

  • Reuses existing screenshot capture pipeline.
  • New dep: none (Pillow likely sufficient; if perceptual diff needs more, consider imagehash or numpy — defer until measurement says we need them).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions