A tag is a filter now, so the vocabulary is closed - #221
Merged
Conversation
195 tags for 170 examples, 145 of them on a single example, 29 examples with none, and 13 whose line in `llms.txt` was the name alone. Three registers were mixed into one field: the technique (`transmission`), the API that implements it (`meshreflectormaterial`), and what the scene depicts (`arkanoid`, `tag-heuer`, `codrops`). The last two are already said by the source and by the title. So: one axis, the technique. Synonyms and plurals merge on the form the catalog already carries more often, ties to the singular. What is left is 117 tags, every one of them on at least one example -- no threshold, which is what keeps `bvh` and `pcss` reachable and keeps every pill from ever returning nothing. The enum lives on `tags` in the schema, the same treatment `libraries` gets, and the validator reads both lists out of that one file. An unknown tag fails the lint rather than warning: a misspelt tag used to be a cosmetic slip, and is now a filter that finds nothing. The editor offers the list while it is typed, since every `pmndrs.json` points `$schema` at it -- which is what makes a closed list bearable. The 31 examples that would have been left with nothing were read, not guessed from their titles: tagging an example from its own title says the title twice. Six terms the catalog did not carry came out of that reading -- fisheye, lod, views, hmr, debug, ssgi. Clicking a tag is the only way into the filter; there is no picker. With most tags on one example a menu of 117 is not something anyone reads, whereas "show me the others like this one" is the actual gesture. Which leaves the badge to say what the picker would have: pressed, it is why the list is short, and clicking it again is how that is undone. It also has to lead its card's strip, or a card can be filtered by a tag its four visible pills do not show. Two things had to move for a pill to be clickable at all. The card was an anchor wrapping everything, and a button inside an anchor is as invalid as an anchor inside one -- so the card keeps its whole clickable rectangle through a stretched link the pills sit above. And the roving tabindex claimed every button in the list, which would have made four tab stops per card out of what is meant to be one; the pills opt out of it and stay a pointer affordance, with the keyboard reaching the same tags from the info panel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Multi-selection, ANDed. Which on its own would be a trap: 117 tags make 6,786 pairs, 336 of which occur together at all and 263 of those on a single example -- so a second click empties the list about nineteen times in twenty, and emptying the list is the one thing the vocabulary was rebuilt to avoid. So the second click is not offered when it would. `TagFilterProvider` indexes the catalog by tag once, `useTagFilter` intersects it with the tags picked so far, and a badge that would leave nothing goes disabled. The dead end stops being something you find by falling into it. It weighs the tags alone, not `?q=` or `?library=`: a pill greyed out because of what sits in the search box reads as broken rather than as narrow. The cards need none of this and get it for free -- a card is in the list because it carries every active tag, so every pill on it has at least that card behind it and can never be the one that empties it. It is the info panel, whose example may not be in the list at all, where a tag can be a dead end. `?tag=physics,game` narrows to 7, and `transmission` on the aquarium goes grey while they are held. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`:active` reaches every ancestor, so toggling a tag sank the whole vignette — a gesture that goes nowhere near the example. The press is the stretched link's now. And the pill says it is pressable: the badge's own hover is written `[a]:hover:`, which lands on anchors, and this one is a button. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CXMeT91HWLjW5Vtd1ySFW5
abernier
marked this pull request as ready for review
August 16, 2026 10:02
kvvasuu
added a commit
to kvvasuu/examples
that referenced
this pull request
Aug 17, 2026
PR pmndrs#221 closed tags to a fixed enum after these examples were tagged. Add ascii, autofocus, brightness-contrast, color-average, glitch, hue-saturation and tone-mapping as new technique tags, and merge depth-of-field/ssao/selective-bloom/ramp onto existing synonyms (dof, ambient-occlusion, selective, gradient+mask).
Merged
krispya
added a commit
that referenced
this pull request
Aug 21, 2026
* Add Autofocus, Bloom and Brightness/Contrast examples, ported from react-postprocessing Autofocus and Brightness/Contrast are faithful ports of their CodeSandbox originals. Bloom's scene was reworked onto the shared Suzanne+Stage template with a glass material and an orbiting light, since the original's licensed (CC-BY-NC-SA) voxel-character model didn't look good and raised licensing questions better avoided. * Add Color Average example, an original scene design No CodeSandbox demo exists for this effect in react-postprocessing's docs. Scene design: a cluster of glossy, palette-colored spheres that drains to grayscale as the ColorAverage effect's opacity ramps up -- gives the grayscale conversion an obvious, satisfying before/after to look at. * Add the e2e test script to the earlier examples' package.json They were added before the e2e/Chromatic test task (from PR #173 and friends) landed on main, so they were missing the "test" script every other example now has. * Add Depth of Field, Glitch and Hue/Saturation examples, ported from react-postprocessing Faithful ports of the CodeSandbox originals, with fixes for two stale prop ranges written against a much older postprocessing version: DepthOfField's focusDistance is documented as normalized [0,1] (the original used 0..4), and HueSaturation's saturation is a -1..1 factor, not radians (same bug fixed earlier in Take Control). Glitch's `mode` prop doesn't reactively update in the current @react-three/postprocessing release (confirmed upstream, fixed in an unreleased 4.0.0) -- swapped it for the effect's other working props (active, strength, delay, duration, columns, ratio) instead. Depth of Field's scene is a row of five Suzanne heads receding in depth rather than the original's single object, so the focus falloff is actually visible; the camera's near/far planes are tightened so the normalized focusDistance/focusRange controls map to a meaningful slice of the scene. * Add Noise, Ramp and Tone Mapping examples, ported from react-postprocessing Noise: grain overlay demo on a staged Suzanne. Ramp: custom gradient-ramp effect masking ASCII/Bloom over a box grid; fixed an invisible-Bloom bug (luminanceThreshold 0.5 -> 0.1 to actually cross the diffuse boxes' luminance). Tone Mapping: ToneMapping effect across all ToneMappingMode values on a staged Suzanne; added a mode dropdown since middleGrey/whitePoint only affect the Reinhard2 family, not the modern AGX default. * Add Selective Bloom example, an original scene design Objects bloom on hover; the torus knot can also be locked on via a Leva checkbox and the sphere via click. Uses @react-three/postprocessing's SelectiveBloom effect with a low luminanceThreshold since selection (not material brightness) drives what glows. * Add SSAO example, an original scene design Dense pile of matte spheres and boxes shows contact-shadow occlusion from @react-three/postprocessing's SSAO effect. Fixed via ref + direct property mutation since the wrapper's useMemo only reconstructs on camera/normalPass change, not on prop updates. Also fixed three tuning issues: the deprecated `radius` setter silently clamps to [1e-6, 1] so the wrapper's own 1-40 default range was a no-op; `bias` and `luminanceInfluence` defaults were tuned for a differently-lit scene and mostly cancelled the effect out on this one. Toggling "enabled" mutes via blendMode opacity rather than unmounting, since EffectComposer renders a black screen when enableNormalPass is on but zero Effect children remain. * Credit original authors in pmndrs.json for the ported and original-design examples * Reconcile postprocessing example tags with the closed tag vocabulary PR #221 closed tags to a fixed enum after these examples were tagged. Add ascii, autofocus, brightness-contrast, color-average, glitch, hue-saturation and tone-mapping as new technique tags, and merge depth-of-field/ssao/selective-bloom/ramp onto existing synonyms (dof, ambient-occlusion, selective, gradient+mask). * Add new examples to the website --------- Co-authored-by: Kris Baumgartner <kjbaumgartner@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
195 tags for 170 examples, 145 of them on a single example, 29 examples with none, and 13 whose line in
llms.txtwas the name alone. This closes the vocabulary at 117 tags, gives every example at least one, and makes the badges the way into a?tag=filter.The design was settled question by question before any of it was written; the two things that need your eyes are the list and the 31 examples that were re-read, both below.
Decisions
examples://indexmeshreflectormaterial,useanimations) and the subject register (arkanoid,codrops,tag-heuer) leaveenumontagsin the schema, exactly whatlibrariesgets. Unknown tag = error, not warning$idWhat to review
Six terms the catalog did not carry, introduced by the re-reading:
fisheye(2) ·lod(2) ·views(2) ·debug(1) ·hmr(1) ·ssgi(1).One call worth contesting:
instancing→instancesfollows the frequency rule (5 vs 2), butinstancesis also drei's component name, which the API-register rule would have evicted. The rule won over the register. Say the word and it flips.The 31 read out of their own source
bvhbvhraycastcanvas-texthtmleventscardsscrollinfinitetextcards-with-border-radiusscrollinfiniteshadercloudsvolumetriccsg-housecsgcontrolssoft-shadowsenvironmentdbismut-furnituretransitionsspringgltfhtmlecctrl-fisheyefisheyephysicscontrolsgameenter-portalsportalroutertextgltfflying-bananaslodgltfdofpostprocessinggltf-animations-re-usedgltfanimationclonespringgltf-animations-tied-to-scrollgltfanimationscrollsoft-shadowsground-projected-envmaps-laminaground-projectionenvironmentcontact-shadowsmaterialimage-galleryreflectionsroutertextmagic-boxportalcontrolsgltfmotionpathcontrolscontrolsbezierpostprocessinggltfmultiple-views-with-uniform-controlsviewscontrolsstatesoft-shadowspairing-threejs-to-uihtmlsuspensecontact-shadowsgltfportalsportalgltfcontact-shadowsenvironmentrapier-ping-pongphysicsgamecollisionsaudiore-using-geometry-and-level-of-detaillodre-usegltfenvironmentroom-with-soft-shadowspcsssoft-shadowsgltfshader-hmrshadermaterialhmrshoppingoutlinespostprocessingbvhmasksimple-physics-example-with-debug-boundsphysicsdebugssgi-spheres-with-rapier-physicsssgiphysicspostprocessingenvironmentthreejs-journey-lv-1-fisheyefisheyecontrolsenvironmentgltfthunder-cloudsvolumetriccamera-shakephysicscontact-shadowstrigger-meshesphysicscollisionsvideo-cookiesvideo-texturegobosoft-shadowspostprocessingview-trackingviewsscrollhtmlcontrolsThe 117, by weight
gltf36 ·physics23 ·html19 ·postprocessing19 ·scroll15 ·transmission15 ·shader14 ·controls13 ·animation12 ·bloom10 ·environment10 ·reflections10 ·soft-shadows9 ·game8 ·instances7 ·portal7 ·contact-shadows6 ·text6 ·audio5 ·material5 ·particles5 ·refraction5 ·spring5 ·annotations4 ·collisions4 ·dof4 ·outlines4 ·volumetric4 ·bvh3 ·camera-shake3 ·csg3 ·events3 ·gradient3 ·ground-projection3 ·infinite3 ·raycast3 ·router3 ·ssr3 ·state3 ·stencil3 ·transitions3 ·ambient-occlusion2 ·bezier2 ·bounds2 ·cell-fracture2 ·clone2 ·configurator2 ·decal2 ·fisheye2 ·gobo2 ·god-rays2 ·hdr2 ·horizontal2 ·interaction2 ·lighting2 ·lod2 ·lut2 ·mask2 ·merging2 ·noise2 ·pcss2 ·re-use2 ·shadows2 ·spotlight2 ·stage2 ·suspense2 ·texture2 ·trail2 ·vertex-colors2 ·vertical2 ·video-texture2 ·views2 ·aspect-ratio1 ·backdrop1 ·baking1 ·barycentric1 ·batching1 ·blur1 ·bones1 ·chromatic-aberration1 ·compression1 ·convex-polyhedron1 ·custom-renderer1 ·debug1 ·depth1 ·distortion1 ·dot-screen1 ·expand1 ·film1 ·flexbox1 ·geometry1 ·gpgpu1 ·grid1 ·highlight1 ·hmr1 ·hud1 ·intersection1 ·lerp1 ·lightformer1 ·lightmap1 ·loading1 ·occlusion1 ·parallax1 ·pixelation1 ·progressive1 ·rectarea-lights1 ·scanline1 ·selection1 ·selective1 ·sepia1 ·snap1 ·soft-particles1 ·ssgi1 ·svg1 ·text-geometry1 ·vignette1 ·wireframe1Merges and renames
gtlf→gltf·clell-fracture→cell-fracture·shaders→shader·animations→animation·portals→portal·instancing→instances·trails→trail·springs→spring·spring-animation→spring·decals→decal·godray→god-rays·softshadows→soft-shadows·shadow→shadows·contact shadows→contact-shadows·scrollcontrols→scroll·scroll-controls→scroll·depth-of-field→dof·markers→annotations·html-annotations→annotations·glow→bloom·fit-all→bounds·grading→lut·gpu→gpgpu·curl→noise·simplex→noise·merged→merging·pointer-events→events·layout→flexbox·svg-renderer→custom-renderer·lights→lighting·spot→spotlight·orbit→controls·orbit-controls→controls·transform→controls·transforms→controls·transform-controls→controls·custom-controls→controls·pointerlock-controls→controls·box projected→ground-projection·ground mapping→ground-projection·cube camera→ground-projection·video→video-textureEvicted — API names, subjects, and category words a sibling tag already said
meshreflectormaterial·rendertexture·useanimations·quadraticbezierline·line2·useintersect·skinnedmesh·meshline·mesh-line·arkanoid·minecraft·pinball·tag-heuer·codrops·baubles·drone·shield·epoxy·diamond·ring·stars·grass·confetti·racing·vehicle·cycling·ragdoll·viewcube·minimap·fire·water·tiles·dome·graph·nodes·image·spline·clouds·frosted-glas·metal·effects·distance·input·analyser·positionalMulti-selection, and why it needs the disabling
Tags AND together. On its own that is a trap: 117 tags make 6,786 pairs, 336 of which occur together at all, and 263 of those on a single example — so a second click empties the list about nineteen times in twenty, which is the one thing the vocabulary was rebuilt to avoid.
So the second click is not offered when it would.
TagFilterProviderindexes the catalog by tag once,useTagFilterintersects that with the tags picked so far, and a badge that would leave nothing goesdisabled. It weighs the tags alone, not?q=or?library=— a pill greyed out because of what sits in the search box reads as broken rather than as narrow.The cards need none of this and get it for free: a card is in the list because it carries every active tag, so every pill on it has at least that card behind it and can never be the one that empties it. It is the info panel, whose example may not be in the list at all, where a tag can be a dead end.
The UI, and the two things that had to move for it
Clicking a tag is the only way in — there is no picker. With most tags on a single example a menu of 117 is not something anyone reads, whereas "show me the others like this one" is the gesture people actually have. The pressed badge is therefore also the answer to "why is this list short" and "how do I undo it", and it leads its card's strip — otherwise a card can be filtered by a tag its four visible pills do not show.
<a>is as invalid as an anchor inside one, so the whole clickable rectangle now comes from a stretched link the pills sit above.buttonin the list, which would have turned one tab stop per card into five. The pills opt out withdata-roving-skipand stay a pointer affordance; the keyboard reaches the same tags from the info panel.NuqsAdaptermoved up to covermain— the info panel is a nuqs consumer now, and its comment said Nav was the only one.Checked
pnpm lint·pnpm lint:metadata(170 examples) ·pnpm format:check·tsc --noEmit·vitest(55) — and by hand in the running site: filter from a card pill (23 forphysics), a second tag on top (?tag=physics,game→ 7),transmissiongoing grey on the aquarium while those two are held, zero disabled pills on any card, filter from the info panel, toggle off, the active tags leading each strip, the stretched link carrying?tag=into the example, and 17 pills attabindex="-1"against exactly one tabbable link in the list.