Skip to content

Add vellum: layered browsing visualization experiment - #351

Draft
spencerc99 wants to merge 5 commits into
mainfrom
claude/layered-browsing-viz-uth5uq
Draft

Add vellum: layered browsing visualization experiment#351
spencerc99 wants to merge 5 commits into
mainfrom
claude/layered-browsing-viz-uth5uq

Conversation

@spencerc99

@spencerc99 spencerc99 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

New visualization experiment at wewere.online/vellum, inspired by printing the cursor/scroll visualizations on translucent vellum paper and overlapping the sheets.

Each unit of browsing — by default one person's visit to one page — becomes a translucent sheet, normalized into a single shared browser-window frame. Sheets stack on a warm paper background and composite with mix-blend-mode: multiply, so overlapping ink darkens the way translucent prints do. Each sheet is composed of plates (bottom → top):

  1. PagePlate — a faint serif domain/title "ghost title" per page; optionally (off by default, see below) a ghosted render of the actual page in a sandboxed, scaled iframe whose scroll position replays the recorded viewport scroll events
  2. ScrollPlate — a hairline viewport-frame band tracking where the person's viewport sat on the page (optional scroll-history ticks)
  3. TrailsPlate — cursor-trail ink in the house style (perfect-freehand tapered outlines, same @movement geometry as the portrait/archive visualizations) plus click/hold marks, progressively drawn during playback

A spread control fans the stack apart (progressive per-sheet offset + slight seeded rotation) so you can peek between the layers, mirroring lifting physical vellum sheets; hovering a fanned sheet lifts it and dims the others.

Screenshots

Captured against deterministic fixture data (the sandbox can't reach the worker); the branch preview shows it with real data.

Full ink (animation off) — riso pigment per sheet, ghost titles overprinting at seeded offsets:
static

Mid-playback — trails reveal progressively; the still-drawing head gets a blunt cap, finished strokes taper:
stacked

Fanned apart (spread 0.55) — cascading captions per sheet, physical shadows:
fanned

Dev panel — every parameter, spec-driven:
devpanel

Domain grouping + mono ink — one sheet per domain, single-pigment monoprint look:
domain-mono

Built as a flexible base

  • Grouping is pluggable (grouping.ts): page-visit / domain / day today; calendar views and other groupings slot in as new modes
  • Plates are a registry (plates/registry.ts) with per-plate isEnabled(settings) predicates: Sheet.tsx iterates it, so future plates (typing, navigation) plug in without touching the compositor
  • Every parameter is spec-driven (settings.ts): ~30 params across data / composition / fan / page / scroll / trails / playback groups, each declared once and auto-rendered in the dev panel (⚙ bottom-right), persisted to localStorage
  • Data comes from the existing useArchiveEvents hook (day picker fed by dayCounts, server-side domain filter supported)

Findings from real-data testing

Fixture data masked three problems that live data surfaced; all fixed here:

  • Page iframes are opt-in (default off). Real sites behind bot protection render Cloudflare challenge pages ("max challenge attempts") inside embedded iframes — a challenge can never complete in a sandboxed, cross-origin, cookie-less iframe, and the failure is undetectable cross-origin. Since the top-of-stack sheets are exactly the iframe-eligible ones, the top of the pile washed out. Ghost titles are decoupled from the page toggle and stay on.
  • Ink defaults to riso (per-sheet pigment). With single-participant real data, participant ink mode gave every sheet the same color, collapsing the stack into one unreadable scribble. Participant mode remains available for multi-person days.
  • Trails use the house style. TrailsPlate now renders SVG perfect-freehand filled outlines with tapered ends via @movement/utils/trailAnimation (shared with the portrait/archive visualizations), and click/hold marks reuse CLICK_DEFAULTS ring+core proportions — instead of the earlier bare canvas strokes.
  • Sheet backing color is a param (sheetColor + sheetOpacity); settings storage key bumped to v2 so the changed defaults override stored v1 values.

Compositing note (the subtle bit)

Fan offsets are applied via left/top, not transform — a transform on the sheet root creates a CSS stacking context, which isolates each sheet's mix-blend-mode and silently kills cross-sheet multiply blending (the entire vellum effect). Rotation still uses transform but only engages while fanned, where sheets have physically separated. Same rule governs why the PagePlate container carries the blend itself (its opacity already isolates its children). Comments in Sheet.tsx/PagePlate.tsx explain this so it doesn't get "optimized" back.

Known limitations

  • domain/day sheets use a first-seen representative URL/color for their page render and ink.
  • The opt-in iframe mode keeps its bot-challenge caveat; there is no cross-origin way to detect a failed embed.

Verification

  • Type-check clean across all vellum files (the website tsconfig's include only covers src/, so this was checked with an explicit include — pre-existing quirk)
  • Visual pass via Playwright against fixture data covering: stacked playback, static full ink, fanned + captions, dev panel, domain grouping + mono ink
  • Website-only change (extension/website/**) — no changeset, no PENDING.md bullet, per repo release conventions

🤖 Generated with Claude Code

https://claude.ai/code/session_01VWX2qwLcDT8B83HBHBmJ1V

New visualization at /vellum: each unit of browsing (person x page visit,
or domain/day groupings) renders as a translucent sheet — ghosted page
iframe, scroll-viewport band, and cursor-trail ink — stacked into one
normalized browser-window frame with multiply blending, like overlapping
prints on vellum paper. A spread control fans the stack apart; every
parameter is adjustable in a spec-driven dev panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWX2qwLcDT8B83HBHBmJ1V
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 4, 2026

Copy link
Copy Markdown

Deploying we-were-online-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 82ae068
Status: ✅  Deploy successful!
Preview URL: https://a6302187.we-were-online-website.pages.dev
Branch Preview URL: https://claude-layered-browsing-viz.we-were-online-website.pages.dev

View logs

@pkg-pr-new

pkg-pr-new Bot commented Aug 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

bun add https://pkg.pr.new/@playhtml/common@351
bun add https://pkg.pr.new/playhtml@351
bun add https://pkg.pr.new/@playhtml/react@351

commit: ad09386

claude added 4 commits August 5, 2026 02:57
- Apply fan offsets via left/top instead of transform: transform creates a
  CSS stacking context, which isolates each sheet's mix-blend-mode and
  silently kills cross-sheet multiply blending (the core vellum effect).
  Rotation still uses transform but only engages while fanned.
- Blend the whole PagePlate container instead of the inner ghost title
  (opacity already isolated the container, so the inner blend never
  reached the stack).
- Compute sheet time bounds with a loop (Math.min spread over a day-sized
  group risks a call-stack RangeError).
- Align scroll-history ticks with the band's travel mapping; gate them
  behind a new showScrollHistory param (default off — they stack into
  barcode noise across sheets).
- Only engage hover-lift once spread > 0.05 so the concentrated stack
  doesn't dim on every mouse move; clear hover when spread returns to 0.
- Quieter defaults after screenshot review: pageOpacity 0.2,
  scrollFrameOpacity 0.35, band fill 0.03; ghost titles now place and
  size deterministically per sheet so stacked titles overprint like
  letterpress runs instead of piling into mush.
- pr-screenshots/: captures for the PR description (removed in a
  follow-up commit; raw URLs stay pinned to this sha).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWX2qwLcDT8B83HBHBmJ1V
The PR body references these images via raw URLs pinned to the previous
commit's sha, which remain valid; the merged tree stays clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWX2qwLcDT8B83HBHBmJ1V
…e trail style

Real-data testing surfaced problems fixture data masked:

- Page iframes default OFF: real sites behind bot protection render
  Cloudflare challenge pages ('max challenge attempts') inside embedded
  iframes — a challenge can never pass in a sandboxed cross-origin
  cookie-less iframe, and the failure is undetectable cross-origin. The
  top-of-stack sheets are exactly the iframe-eligible ones, so the top
  of the pile got washed out. Ghost titles are decoupled from the page
  toggle (registry now gates plates via an isEnabled predicate).
- Ink defaults to riso (per-sheet pigment): with single-participant real
  data, participant mode gave every sheet the same color, collapsing the
  stack into one unreadable scribble.
- Sheet backing color is now a param (sheetColor + existing sheetOpacity)
  instead of hardcoded white.
- TrailsPlate rewritten from canvas strokes to the house cursor-trail
  style: SVG + perfect-freehand filled outlines with tapered ends via
  @movement/utils/trailAnimation, blunt cap on the still-drawing head,
  click/hold marks reusing CLICK_DEFAULTS ring+core proportions.
- Settings storage key bumped to v2 so changed defaults override stored
  v1 values.
- pr-screenshots/: refreshed captures (removed in follow-up commit; PR
  body pins raw URLs to this sha).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWX2qwLcDT8B83HBHBmJ1V
PR body pins raw URLs to the previous commit's sha; tree stays clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWX2qwLcDT8B83HBHBmJ1V
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.

2 participants