Skip to content

trough rendering: 3/4-perspective triangular prism vessel - #6

Merged
jackhenderson12 merged 1 commit into
mainfrom
trough-rendering
May 7, 2026
Merged

trough rendering: 3/4-perspective triangular prism vessel#6
jackhenderson12 merged 1 commit into
mainfrom
trough-rendering

Conversation

@jackhenderson12

Copy link
Copy Markdown
Contributor

Summary

The 2D outlined funnel was mathematically right but visually bare — thin stroke, no material, no depth, no rim, no base. It read as a math diagram, not a vessel. This swap renders each ballot item as a 3D triangular trough (V cross-section extruded perpendicular into the page) viewed in 3/4 perspective. Users now see a thing-that-holds-water — rim, walls, depth, water surface — without any change to what the math is doing.

Why a prism, not a cone

A cone's volume scales with h³ → that would be cubic voting. A triangular prism (the V cross-section extruded by a constant depth D) keeps volume proportional to h² × D. With the math layer treating D as 1, the QV identity credits = votes² is preserved exactly. The 3D appearance is a rendering decision; the conservation math is unchanged.

Implementation

Pure SVG. No Canvas, WebGL, or 3D libraries. Hand-authored paths with a parallel projection from 3D (x, y, z) to 2D screen:

screen_x = x − z · sin(~18°)    // back peeks LEFT of front (yaw)
screen_y = y − z · sin(~9°)     // back peeks UP of front (pitch)

Yaw ~18° gives a clear right-side panel; pitch ~9° gives a clear parallelogram for the rim and water surface (no flat lines).

Layered z-order (back to front):

  1. Soft drop shadow (Gaussian blur of dark ellipse) + base footing ellipse — grounds the trough on a surface.
  2. Back-face V triangle — faint; peeks above the front rim.
  3. Water body — V at the current display level. Drawn behind the front wall so wall translucency tints the visible water.
  4. Water surface — parallelogram from front-edge-of-water to back-edge-of-water at level h, with a vertical gradient and a thin lighter stroke along the front edge so it reads as a liquid plane, not a polygon.
  5. Right side panel — quadrilateral with a top-to-bottom gradient (slightly darker than the front face) for depth.
  6. Front face V — semi-translucent fill (water shows through), stroked outline.
  7. Top rim band — outer parallelogram with inner-rim cutout (even-odd fill) ⇒ visible wall thickness. One of the strongest "vessel" cues; the inner rim is parallel-shifted by wt ≈ 4.5 % of trough height.

Per-instance SVG ids (filter, gradients) keyed off useId so the six funnels don't collide.

What's preserved

  • All math (costForVotes, maxVotes, clampVotesAgainstBudget, conservation).
  • All interaction (hold-to-pour, continuous real-valued votes, no integer snap, no tap shortcut).
  • The pool, the pour stream, the reservoir.
  • The intro copy, the "How it works" explainer, the framing prompt, the footer disclaimer, the default ballot.
  • instantUpdate / Framer Motion split: during a live hold the water body and surface render the rAF-driven votes prop frame-for-frame; outside a hold, transitions get the same 0.18 s cubic-bezier settle as before.
  • The word "funnel" throughout the codebase and UI strings — the form is funnel-like (V, narrow at the bottom, opening at the top); we don't need to rename it just because the cross-section is rectangular instead of circular.

Verified

  • Empty (0 votes): the trough reads as a 3D vessel — rim parallelogram with thickness, right-side panel slightly darker than front face, base footing ellipse, soft drop shadow. No water visible.
  • Mid-fill (~4.5 votes / 20 credits): water body fills the apex pyramid, surface plane parallelogram visible at level h with vertical gradient and white front-edge highlight, right side panel partly occludes water on the right (depth read).
  • At cap (10 votes / 100 credits): water reaches rim level, surface plane sits just under the rim band; pool reads 0.0 / 100 credits.
  • Conservation: pool + Σ votes² = budget at every render frame and dispatch (29 unit tests still pass).
  • Toolchain: ESLint clean, tsc -b --noEmit clean, all three build targets succeed (SPA / lib / Web Component), no console warnings during interaction.

Files changed

Just one: src/components/Funnel.tsx. Component prop interface unchanged. Math layer unchanged. Reducer unchanged. PourControl, PourStream, CreditPool, LiquidQV, Intro, Explainer, PageChrome — all unchanged.

Visual quick-tour

The screenshots aren't embedded in this PR body (GitHub doesn't accept binary uploads via gh pr create). To see the new rendering, pull the branch and npm run dev. From a fresh page:

  1. The grid shows six empty 3/4-perspective troughs. Each has a visible rim band (with thickness), a foreshortened right side panel (slightly darker), a base disc, and a soft drop shadow.
  2. Hold + on Kamala Harris for ~1 second. As the water rises, you'll see:
    • A small water-body pyramid form at the apex
    • A surface plane parallelogram with a gradient + white front-edge highlight
    • The pool decimal readout drop in real time (e.g., 94.3 / 100 credits)
  3. Continue holding. The water surface rises non-linearly (the felt quadratic). At the cap (~20 s of hold) the surface sits just below the rim band.
  4. Release. Pool snaps to whatever was reached. Stream fades. The 3D rendering is identical at rest and during a live hold — only the water level changes.

Test plan

  • npm install && npm run dev
  • Verify empty troughs read as 3D vessels (rim, side panel, base, shadow visible)
  • Hold + on a funnel; verify water body + surface plane both visible and animating with the rAF tick
  • Hold to cap; verify the water plane sits just under the rim band
  • Release; verify the visual state at rest matches what was visible mid-hold
  • Tab + Space hold on a focused funnel; same continuous pour
  • Reduced-motion: water body still renders correctly; live frame-updates suppressed
  • npm test && npm run lint && npm run typecheck — green
  • npm run build:all && npm run preview — clean

Don't merge

Per the brief: hold for review.

🤖 Generated with Claude Code

The 2D outlined funnel was mathematically right but visually bare —
thin stroke, no material, no depth, no rim, no base. It read as a
math diagram, not a vessel. This swap renders each ballot item as
a 3D triangular trough (V cross-section extruded perpendicular into
the page) viewed in 3/4 perspective. Users now see a thing-that-
holds-water — rim, walls, depth, water surface — without any change
to what the math is doing.

Why a prism, not a cone
-----------------------
A cone's volume scales with h³ → that would be cubic voting. A
triangular prism (the V cross-section extruded by a constant depth
D) keeps volume proportional to h² × D. With the math layer treating
D as 1, the QV identity `credits = votes²` is preserved exactly.
The 3D appearance is a rendering decision; the conservation math
is unchanged.

Implementation
--------------
Pure SVG. No Canvas, WebGL, or 3D libraries. Hand-authored paths
with a parallel projection from 3D (x, y, z) to 2D screen:

    screen_x = x − z · sin(~18°)    // back peeks LEFT of front
    screen_y = y − z · sin(~9°)     // back peeks UP of front

Yaw ~18° gives a clear right-side panel; pitch ~9° gives a clear
parallelogram for the rim and water surface (no flat lines).

Layered z-order (back to front):
  1. Soft drop shadow (Gaussian blur of dark ellipse) + base footing
     ellipse — grounds the trough on a surface.
  2. Back-face V triangle — faint; peeks above the front rim.
  3. Water body — V at the current display level. Drawn behind the
     front wall so wall translucency tints the visible water.
  4. Water surface — parallelogram from front-edge-of-water to
     back-edge-of-water at level h, with a vertical gradient and a
     thin lighter stroke along the front edge so it reads as a
     liquid plane, not a polygon.
  5. Right side panel — quadrilateral with a top-to-bottom gradient
     (slightly darker than the front face) for depth.
  6. Front face V — semi-translucent fill (water shows through),
     stroked outline.
  7. Top rim band — outer parallelogram with inner-rim cutout
     (even-odd fill) ⇒ visible wall thickness at the rim. One of
     the strongest "vessel" cues; the inner rim is parallel-shifted
     by `wt` perpendicular to the wall (≈ 4.5 % of trough height).

Interaction, animation, ARIA, prop interface — all unchanged from
the 2D version. The water body and surface both run through the
existing `instantUpdate` / Framer Motion split: during a live hold
they render the rAF-driven `votes` prop frame-for-frame; outside a
hold, transitions get the same 0.18 s cubic-bezier settle as before.

Per-instance SVG ids (filter, gradients) keyed off `useId` so the
six funnels on the page don't collide.

Verified
--------
- Empty trough at 0 votes reads as a 3D vessel with rim, walls,
  base, and shadow — no water visible (water polygons collapse to
  degenerate paths, still valid for Framer Motion).
- Mid-fill at 4.5 votes / 20 credits: water body fills the apex
  pyramid, surface plane parallelogram visible at level h, white
  edge highlight along the front, and the right side panel partly
  occludes water on the right (depth read).
- Full at the cap (10 votes / 100 credits): water reaches the rim
  level, surface plane sits just under the rim band.
- Pool, conservation invariant, hold-to-pour, stream visual,
  reduced-motion, keyboard parity — all unaffected.
- 29 tests passing, ESLint clean, typecheck clean, all three build
  targets succeed, no console warnings during interaction.

What didn't change
------------------
- All math (`costForVotes`, `maxVotes`, `clampVotesAgainstBudget`,
  conservation).
- All interaction (hold-to-pour, continuous values, no integer snap).
- The pool, the pour stream, the reservoir.
- The intro copy, the "How it works" explainer, the framing prompt,
  the footer disclaimer, the default ballot.
- The word "funnel" throughout the codebase and UI strings — the
  rendered form is funnel-like (V, narrow at the bottom, opening
  at the top); we don't need to rename it just because the
  cross-section is rectangular instead of circular.
@jackhenderson12
jackhenderson12 merged commit 7b0901b into main May 7, 2026
1 check passed
jackhenderson12 added a commit that referenced this pull request May 7, 2026
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.

1 participant