{"id":50496946,"url":"https://github.com/elerac/prismifold","last_synced_at":"2026-06-02T08:30:24.949Z","repository":{"id":350814515,"uuid":"1208349404","full_name":"elerac/prismifold","owner":"elerac","description":"A multichannel image viewer for high-dimensional light data: polarization, spectral, panoramas, depth, AoVs.","archived":false,"fork":false,"pushed_at":"2026-06-02T06:17:35.000Z","size":9398,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-02T06:26:11.764Z","etag":null,"topics":["depth-map","hyperspectral","image-viewer","mueller-matrix","openexr","panorama","polarization","spectral","stokes-vector"],"latest_commit_sha":null,"homepage":"https://elerac.github.io/prismifold/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elerac.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-12T06:37:07.000Z","updated_at":"2026-06-02T06:17:39.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/elerac/prismifold","commit_stats":null,"previous_names":["elerac/openexr_viewer","elerac/prismifold"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/elerac/prismifold","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elerac%2Fprismifold","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elerac%2Fprismifold/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elerac%2Fprismifold/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elerac%2Fprismifold/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elerac","download_url":"https://codeload.github.com/elerac/prismifold/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elerac%2Fprismifold/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33814306,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["depth-map","hyperspectral","image-viewer","mueller-matrix","openexr","panorama","polarization","spectral","stokes-vector"],"created_at":"2026-06-02T08:30:24.298Z","updated_at":"2026-06-02T08:30:24.942Z","avatar_url":"https://github.com/elerac.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Prismifold\n\nPrismifold is a multichannel image viewer for computational imaging, rendering, and vision workflows. It unfolds polarization, spectral, panoramas, depth, AoVs and exact pixel values from images with more than color.\n\n[![Prismifold thumbnail](https://elerac.github.io/prismifold/thumbnail.jpg)](https://elerac.github.io/prismifold/app/)\n\n## Features\n\n- OpenEXR decode via a browser-safe `exrs` WASM adapter with full layer/channel extraction.\n- Gallery samples: local `Gallery \u003e cbox_rgb.exr`, plus remote `Gallery \u003e Beachball \u003e multipart.0001.exr`, Poly Haven HDRIs under `Gallery \u003e Poly Haven`, KAIST hyperspectral samples under `Gallery \u003e KAIST Hyperspectral`, and Polanalyser Stokes samples under `Gallery \u003e Polanalyser`; remote samples require network access.\n- Local EXR load via `File \u003e Open...` or drag/drop (drag-and-drop supports multiple files and recursive folder drops in one action).\n- Recursive folder EXR load via `File \u003e Open Folder...`; all `.exr` files under the selected folder are appended as sessions.\n- `File \u003e Export...` exports the full active display to PNG at display image size with configurable PNG compression and current channel/stokes, exposure/gamma, colormap, and alpha settings applied.\n- `File \u003e Export Screenshot...` exports an image-viewer or panorama-viewer screenshot region to PNG; multiple screenshot regions export as a ZIP, with optional reproduction JSON.\n- `File \u003e Export Batch...` exports selected file/channel combinations as a ZIP of PNG images.\n- `File \u003e Export Colormap...` exports any registered colormap as a standalone PNG gradient with configurable colormap, size, orientation, and filename.\n- Right-click `Copy Image` copies the current display image to the clipboard.\n- `View \u003e Image viewer` / `Panorama viewer` switches between the existing 2D image view and an equirectangular panorama projection suitable for 360-degree environment maps and HDRIs.\n- `View \u003e Rulers` toggles pixel rulers in `Image viewer`.\n- `Window` controls include normal/full-screen preview plus single-pane, vertical split, and horizontal split viewer layouts.\n- Top-bar quick actions include Auto Fit, Auto Exposure, invalid-value warning, screenshot export, Metadata, app fullscreen, and the Settings gear.\n- `Shift` + left-drag in `Image viewer` creates or replaces a persistent rectangular ROI for measurement; drag an existing ROI body or handles to edit it. ROI creation/editing is disabled in `Panorama viewer`.\n- Multi-image sessions:\n  - New image opens as active while previously opened images are kept in memory.\n  - `Open Files` list allows switching active image by filename; rows show thumbnails/status and support filtering, inline rename, drag reorder, and drag-to-viewer-pane assignment.\n  - Multi-layer EXR state is preserved per opened session. Display channel mapping, the active probe position, and the committed ROI carry across session switches when valid for the target image. The active viewer mode is preserved across session switches, and each session remembers separate image-view and panorama-view camera state.\n  - When Auto Fit selected images is enabled, image-mode session switches and new loads fit to the viewer instead of carrying previous pan/zoom; this does not apply in `Panorama viewer`. Colormap state carries only when the display selection remains compatible.\n  - Decoded CPU pixels are included in the displayed memory usage. The LRU budget evicts retained display CPU/GPU channel resources, so decoded-only usage can exceed the selected cap. The default cap is `256 MB`, configurable from `Settings` dialog \u003e `Memory Budget` with fixed presets (`64`, `128`, `256`, `512`, `1024` MB).\n  - Per-file row `Reload` action re-decodes the selected session from its original source.\n  - `File \u003e Reload All` re-decodes all opened sessions from their original sources.\n  - Per-file row `Close` action closes the selected filename entry.\n  - `File \u003e Close All` closes all opened sessions at once.\n  - Duplicate filenames are disambiguated as `name.exr (2)`, `name.exr (3)`, etc.\n- Visible loading indicator while large EXR files are decoding/loading.\n- ROI inspector:\n  - Shows bounds, size, total pixels, per-channel valid sample counts, and `min` / `mean` / `max` for the active display selection.\n  - ROI survives view-mode switches, carries across image switches and new loads, clamps to the target image bounds when needed, and can be cleared from the Inspector.\n- Display controls:\n  - `None` is the default RGB display path and exposes Exposure and Gamma controls. Exposure uses slider + numeric input (`-10` to `+10` EV, step `0.1`).\n  - `Colormap` maps current display luminance over the full active image through the selected NumPy LUT palette.\n  - Built-in palettes are listed in `public/colormaps/manifest.json` and stored as static `.npy` files in the same directory.\n  - The app accepts LUT arrays with shape `(N, 3)` or `(N, 4)` and dtype `float32`, `float64`, or `uint8`.\n  - RGB Exposure/Gamma controls are hidden in `Colormap` mode; colormap mode exposes separate EV/Gamma controls that affect LUT mapping.\n  - `Palette` selects the active LUT without rebuilding the EXR display texture.\n  - `vmin`/`vmax` can be adjusted with one dual-handle slider or numeric inputs.\n  - `Auto Range` has two modes: highlighted always-auto mode follows each image/layer/channel, while one-time/manual mode preserves the current min/max across targets. Dynamic auto ranges use `v=max(abs(min), abs(max))` and map to `[-v, v]`.\n  - Selecting a diverging palette auto-enables `Zero Center`, which keeps manual ranges symmetric around zero (`min=-v`, `max=v`) and also applies to fixed Stokes colormap defaults.\n  - `Reverse` flips the active colormap ramp.\n  - Angle Stokes colormaps expose a paired degree modulation toggle: AoLP can be modulated by DoLP, CoP by DoCP, and ToP by DoP. AoLP also lets the modulation target be `V` (HSV value) or `S` (HSV saturation), defaulting to `V`. CoP and ToP modulation default to on; AoLP defaults to off.\n  - Leaves raw numeric probe values unchanged.\n- Nearest-neighbor rendering at all zoom levels (no interpolation).\n- Zoom range: `0.03125x` to `512x`, wheel zoom anchored to cursor.\n- Pan with left mouse drag.\n- Panorama viewer:\n  - Projects the current display texture onto a sphere using equirectangular sampling.\n  - Left drag orbits the camera; `W/A/S/D` also orbit yaw/pitch; mouse wheel changes horizontal FOV from `1` to `180` degrees, with the widest range transitioning to a hemispherical projection.\n  - The Inspector probe remains available through panorama ray-to-pixel lookup.\n  - Existing ROIs remain stored but cannot be created or edited until you return to `Image viewer`.\n  - Panorama mode does not draw on-canvas pixel value overlays.\n  - On-canvas probe rectangles remain hidden in panorama mode.\n- Probe:\n  - Hover pixel readout in the Inspector.\n  - Click to lock/unlock probe pixel.\n  - Values are raw linear EXR channel values (pre-exposure, pre-display transform).\n- Metadata:\n  - The top-bar Metadata dialog shows EXR header metadata for the active image/layer, including common attributes such as compression, data/display windows, line order, channels, type, capture date, renderer/integrator, and compatible custom attributes.\n- On-image pixel labels at high zoom:\n  - RGB values shown inside image pixels.\n  - 3-channel values stacked vertically.\n  - Label colors follow channel mapping (`R`, `G`, `B`).\n  - Panorama mode reuses the same value formatting, but only draws labels for source pixels with a stable, sufficiently large projected footprint.\n- Channel controls:\n  - Bottom channel thumbnail strip selects grouped channels such as `HOGE.R/G/B`, `FUGA.R/G/B`, `normal.X/Y/Z`, and `motion.U/V`; grouped RGB remains the default display when available, while XYZ and UV groups are used when no RGB group is available. XYZ maps `X/Y/Z` to display red/green/blue, and UV maps `U/V` to display red/green with blue fixed at zero.\n  - Alpha is applied to normal channel displays when a matching companion exists: bare `R/G/B` and bare scalar channels use bare `A`, while namespaced channels such as `beauty.R` or `depth.Z` use `beauty.A` or `depth.A`. Collapsed channel choices group alpha into labels such as `RGBA`, `mask,A`, and `beauty.RGBA` instead of showing the companion alpha separately.\n  - Auxiliary channels such as `Z`, masks, and custom AOVs are selectable directly and display as grayscale by mapping that source channel into all three display channels, which makes `Colormap` operate on that channel directly.\n  - Expandable channel stacks expose split component entries for RGB, XYZ, and UV groups plus `A` when alpha exists. Scalar alpha pairs such as `mask,A` expose separate `mask` and `A` entries in expanded stacks.\n  - Spectral wavelength series are grouped into a `Spectral RGB` entry by default using the built-in spectral-to-RGB conversion; expandable stacks expose individual wavelength channels.\n  - Stokes layers with `S0/S1/S2/S3` expose derived `Stokes S1/S0`, `Stokes S2/S0`, `Stokes S3/S0`, `Stokes AoLP`, `Stokes DoP`, `Stokes DoLP`, `Stokes DoCP`, `Stokes CoP`, and `Stokes ToP` entries. Complete non-RGB suffixed sets such as `S0.Y/S1.Y/S2.Y/S3.Y` are also exposed as scalar Stokes entries with suffixed labels such as `Stokes AoLP.Y`, while spectral Stokes sets such as `S0.500nm/S1.500nm/S2.500nm/S3.500nm` are grouped into entries such as `S1/S0 Spectral RGB` and expanded into per-wavelength entries such as `S1/S0.500nm`. Scalar AoLP uses HSV over `[0, pi]`; degree parameters use Black-Red over `[0, 1]`; CoP and ToP use signed ellipticity angle over `[-pi/4, pi/4]`. CoP enables `Zero Center` by default. Switching within the same Stokes colormap group, such as DoP/DoLP/DoCP or S1/S0/S2/S0/S3/S0, preserves the current palette, `vmin`/`vmax`, auto/manual mode, and zero-center setting.\n  - RGB Stokes layers with `S0.R/G/B` through `S3.R/G/B` expose grouped `S1/S0.RGB`, `S2/S0.RGB`, `S3/S0.RGB`, `AoLP.RGB`, `DoP.RGB`, `DoLP.RGB`, `DoCP.RGB`, `CoP.RGB`, and `ToP.RGB` entries. In `None`, grouped entries derive the selected Stokes parameter independently for `R`, `G`, and `B`; in `Colormap`, grouped entries keep the Rec.709 mono-derived visualization. Expanded stacks expose per-component entries such as `S1/S0.R`, `AoLP.G`, and `DoP.B`.\n  - Mueller matrix layers with complete `M00` through `M33` channel sets expose a `Mueller Matrix` entry rendered as a row-major 4x4 grayscale grid with no separator pixels. Complete non-RGB suffixed sets such as `M00.Y` through `M33.Y` expose suffixed entries such as `Mueller Matrix.Y`. RGB Mueller sets with `M00.R/G/B` through `M33.R/G/B` expose a grouped `Mueller Matrix.RGB` entry, and expanded stacks expose per-component entries such as `Mueller Matrix.R`.\n  - When a selected layer does not expose the previous channel mapping, the viewer falls back to the first non-Mueller RGB group, then RGB Mueller, then the first RGB group, then XYZ/UV, then spectral RGB when available, then exact `Y`, then grayscale, then a complete Mueller matrix grid, then the first non-alpha channel.\n- Double-clicking the Display heading resets visualization mode/palette, RGB exposure/gamma, colormap EV/gamma/range/zero-center/reverse, without changing channel selection or view.\n\n## UI Layout\n\n- Left panel: `Open Files`.\n- Center: image viewer canvas.\n- Bottom panel: channel thumbnails.\n- Right side: Display, Probe, Spectral, ROI, View, and Image Stats panels.\n\n## Tech Stack\n\n- Vite + Vanilla TypeScript\n- WebGL2 renderer\n- `exrs` (WASM OpenEXR decoder)\n- Vitest (unit/integration-style tests)\n- Playwright (workflow E2E)\n\n## Requirements\n\n- Node.js 20+\n- npm\n- Modern browser with WebGL2\n\n## Local Development\n\n```bash\nnpm install\nnpm run dev\n```\n\nOpen the local Vite URL (usually `http://localhost:5173`) for the project page, or `http://localhost:5173/app/` for the viewer app.\n\n## Build\n\n```bash\nnpm run build\nnpm run preview\n```\n\nOutput is generated in `dist/` and is static-hosting ready.\n\n## Desktop App\n\nPrerequisites:\n\n- Node.js 20+ and npm 10+\n- Rust stable (`rustc` and `cargo`)\n- Tauri platform prerequisites for your OS\n\nBuild the desktop app locally:\n\n```bash\nnpm install\nnpm run desktop:build\n```\n\nOn macOS, the local unsigned app and DMG are generated under `src-tauri/target/release/bundle/`. Generated desktop bundles and build outputs should stay uncommitted.\n\nOn Windows, the installed executable is `Prismifold.exe`. The installer registers Prismifold as an OpenEXR `.exr` handler candidate, but Windows requires the user to choose the default app: open **Settings \u003e Apps \u003e Default apps \u003e Choose defaults by file type \u003e .exr** and select Prismifold, or right-click an `.exr` file and use **Open with**. Older builds may not appear automatically; browse to the installed `Prismifold.exe` if needed.\n\n## GitHub Pages\n\nThis project is prepared for GitHub Pages with a project page and app route:\n\n```text\nProject page: https://elerac.github.io/prismifold/\nViewer app:   https://elerac.github.io/prismifold/app/\n```\n\nOpen a remote EXR directly with `?src=\u003cexr-url\u003e`, for example `https://elerac.github.io/prismifold/app/?src=https://elerac.github.io/prismifold/cbox_rgb.exr`. The EXR host must allow browser CORS requests.\n\nGitHub Pages should use GitHub Actions as the publishing source. The repository now uses a dedicated `CI` workflow for lint, typecheck, coverage, Playwright, and build checks on pushes, and the Pages workflow deploys only after `CI` succeeds on `main` or when triggered manually. The Pages build runs with `GITHUB_PAGES=true`, which sets the Vite base path to `/prismifold/`, builds the landing page at the project root and the viewer at `/app/`, uploads the generated `dist/` directory as the Pages artifact, and deploys it. Keep `dist/` uncommitted; it is generated by the action.\n\n## Tests\n\nRun the local quality gates:\n\n```bash\nnpm run lint\nnpm run typecheck\nnpm run test\nnpm run test:coverage\n```\n\nRun Playwright E2E tests:\n\n```bash\nnpx playwright install\nnpm run test:e2e\n```\n\n## HTML and JS Embeds\n\nThe embed wrapper registers `\u003cprismifold-viewer\u003e` and `window.Prismifold`.\n\n### HTML custom element\n\nLoad the deployed wrapper script, then add the custom element:\n\n```html\n\u003cscript src=\"https://elerac.github.io/prismifold/embed/prismifold.js\"\u003e\u003c/script\u003e\n\n\u003cprismifold-viewer\n  src=\"https://elerac.github.io/prismifold/cbox_rgb.exr\"\n  name=\"Cornell Box\"\n  width=\"640\"\n  height=\"420\"\u003e\n\u003c/prismifold-viewer\u003e\n```\n\nCommon attributes:\n\n| Attribute | Description |\n| --- | --- |\n| `src` | EXR URL to load. |\n| `name` | Embedded source label and opened file name when applicable. |\n| `view` | Initial mode: `image`, `panorama`, or `depth`. |\n| `auto-load` | Set to `false` to defer loading until the embedded `Load image` button is clicked. Defaults to `true`. |\n| `width` / `height` | CSS sizes; numeric values become pixels. Defaults: `100%` / `320px`. |\n| `viewer-url` | Viewer deployment URL, needed if the wrapper script is served from another location. |\n| `source-origin` | Loading policy: `auto`, `parent`, or `viewer`. |\n| `bottom-panel` | Embed bottom content: `probe`, `channels`, or `none`. Defaults to `probe`. |\n\nThe embed supports pan, zoom, hover probe or compact channel selection, and an `Open full viewer` button.\n\n### JavaScript API\n\nUse `Prismifold.create(target, options)` for dynamic sources:\n\n```html\n\u003cdiv id=\"viewer\"\u003e\u003c/div\u003e\n\u003cscript src=\"https://elerac.github.io/prismifold/embed/prismifold.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  const viewer = Prismifold.create('#viewer', {\n    src: './public/cbox_rgb.exr',\n    name: 'Cornell Box',\n    width: 640,\n    height: 420,\n    bottomPanel: 'channels',\n    autoLoad: true,\n    viewerUrl: 'https://elerac.github.io/prismifold/app/'\n  });\n\u003c/script\u003e\n```\n\nController methods:\n\n| Method | Description |\n| --- | --- |\n| `loadUrl(src, options)` | Load an EXR URL; options include `name`, `view`, and `sourceOrigin`. |\n| `loadFile(file, options)` | Load a browser `File` from user-selected or dropped files. |\n| `setView(view)` | Switch to `image`, `panorama`, or `depth`. |\n| `destroy()` | Remove the embedded viewer. |\n\n### Source loading and CORS\n\n- In `auto` mode, relative and `blob:` URLs are fetched by the embedding page; absolute remote URLs load in the viewer.\n- Use `source-origin=\"parent\"` / `sourceOrigin: 'parent'` or `source-origin=\"viewer\"` / `sourceOrigin: 'viewer'` to force\n  either side.\n- Remote viewer-loaded files must be CORS-readable by the viewer origin, such as `https://elerac.github.io`.\n- Direct `file://` relative EXR loading is not browser-portable. Use a local HTTP server for URL-based examples, or\n  `loadFile(file)` for local files.\n\n## Controls\n\n- `Open Files` list: switch active image session by filename, filter rows, rename rows inline, or drag rows to reorder/assign to a split pane.\n- `Alt/Option+Up/Down`: reorder the active `Open Files` row.\n- `Gallery \u003e cbox_rgb.exr` / `Beachball \u003e multipart.0001.exr` / `Poly Haven` / `KAIST Hyperspectral` / `Polanalyser`: open a gallery sample and append it as a new session. Remote samples require network access.\n- `File \u003e Open...`: open one EXR file and append it as a new session.\n- `File \u003e Open Folder...`: recursively open every `.exr` file under the selected folder and append them as new sessions.\n- Drag/drop: drop one or more `.exr` files, or drop a folder to recursively load every `.exr` under it.\n- `File \u003e Export...`: export the active full display to PNG at display image size, with configurable PNG compression.\n- `File \u003e Export Screenshot...`: select and export screenshot regions from Image or Panorama viewer; multiple regions export as a ZIP.\n- `File \u003e Export Batch...`: export selected file/channel combinations as a ZIP of PNG images; the batch dialog has its own `Split RGB` option.\n- `File \u003e Export Colormap...`: export a registered colormap to a PNG gradient with selectable colormap, `width`, `height`, `orientation`, and filename.\n- Right-click viewer menu \u003e `Copy Image`: copy the current display image to the clipboard.\n- Settings dialog \u003e `Memory Budget`: choose the retained display CPU/GPU residency budget from `64`, `128`, `256`, `512`, or `1024` MB. Displayed usage also includes decoded CPU pixels. The value persists in `localStorage`.\n- Settings dialog: configure theme, spectrum lattice motion, spectral grouping default, Stokes defaults/visibility, invalid Stokes masking, auto exposure percentile, and image load workers.\n- `View \u003e Image viewer` / `Panorama viewer`: switch between planar image viewing and spherical panorama viewing.\n- `View \u003e Rulers`: toggle pixel rulers in Image viewer.\n- `Window \u003e Full Screen Preview`: show the viewer in browser fullscreen/fallback preview mode.\n- `Window \u003e Single Pane` / `Split Vertically` / `Split Horizontally`: reset or split the viewer panes. `Cmd+D` splits vertically, and `Cmd+Shift+D` splits horizontally.\n- Per-file row `Reload` action: reload and re-decode that entry in `Open Files`.\n- `File \u003e Reload All`: reload and re-decode all opened image entries.\n- Per-file row `Close` action: close that entry in `Open Files`.\n- `File \u003e Close All`: close all opened image entries.\n- Mouse wheel: zoom around cursor.\n- `+` / `-`: zoom in/out.\n- Left drag: pan in Image viewer.\n- `W/A/S/D`: pan in Image viewer.\n- In `Panorama viewer`, mouse wheel changes horizontal FOV, left drag orbits yaw/pitch, and `W/A/S/D` also orbit yaw/pitch.\n- `Ctrl/Cmd+S`: open `File \u003e Export...`.\n- Hover: live probe sample.\n- Left click: lock/unlock probe.\n- `Shift` + left drag in `Image viewer`: create or replace the current ROI; drag the existing ROI body/handles to edit it.\n\n## Implementation Notes\n\n- Display path: normal RGB uses `linear * 2^EV`, then display-gamma encode for screen; colormap mode maps display luminance through the selected `.npy` LUT after colormap EV/gamma, range, zero-center, and reverse settings. Channel-display alpha is composited over the viewer checkerboard on screen in both RGB and colormap modes; exports preserve image alpha when present. When split component entries are selected, separate `R`, `G`, and `B` channel choices duplicate the selected source into RGB, so display luminance equals that channel value. Grouped XYZ uses the same direct component display path as RGB, and grouped UV binds `U` and `V` to red and green while leaving blue at zero. Split component Stokes entries derive the selected parameter from only the chosen component's Stokes channels before duplicating the scalar into RGB. Grouped RGB Stokes entries derive `R`, `G`, and `B` independently in `None`, but collapse to the existing Rec.709-derived mono path in `Colormap`. For angle Stokes modulation, the LUT color is converted to HSV, its value component is multiplied by the clamped paired degree value, and the result is converted back to RGB; AoLP can instead multiply HSV saturation when `S` modulation is selected.\n- Panorama path: the same display texture is reused, but the fragment shader interprets it as an equirectangular environment map, casts a view ray from yaw/pitch/HFOV, and fetches the matching source pixel with nearest-neighbor sampling before applying the normal RGB or colormap display transform.\n- Colormap authoring in Python:\n  ```python\n  import numpy as np\n\n  lut = np.array([\n      [1.0, 0.0, 0.0],\n      [0.0, 0.0, 0.0],\n      [0.0, 1.0, 0.0],\n  ], dtype=np.float32)\n\n  np.save(\"public/colormaps/red_black_green.npy\", lut)\n  loaded = np.load(\"public/colormaps/red_black_green.npy\")\n  ```\n  Register the file in `public/colormaps/manifest.json`:\n  ```json\n  {\n    \"colormaps\": [\n      {\n        \"label\": \"Red / Black / Green\",\n        \"file\": \"red_black_green.npy\"\n      }\n    ]\n  }\n  ```\n- Texture sampling uses `NEAREST` for both `MIN_FILTER` and `MAG_FILTER`.\n- EXR WASM is initialized through a local adapter module backed by a vendored wasm loader, avoiding app-level deep imports into `exrs` internals.\n- EXR metadata is parsed directly from header bytes before pixel decode because the current WASM decoder only exposes dimensions, layers, channels, and pixel data. Metadata parse failures do not block image loading.\n- Performance path for large images/channel sets:\n  - channel thumbnail DOM updates are throttled to selection/image changes only,\n  - decoded CPU pixels are tracked in displayed memory usage, while the configurable LRU budget evicts retained display CPU/GPU channel resources with eviction protection limited to the currently bound display channels,\n  - the active display texture buffer is reused across channel and layer switches,\n  - GPU upload uses `texSubImage2D` for same-size updates.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felerac%2Fprismifold","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felerac%2Fprismifold","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felerac%2Fprismifold/lists"}