brane 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +367 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +101 -0
- package/DESIGN.md +405 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +213 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +360 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1101 -0
- package/dist/commands/token.js +154 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6353 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +643 -0
- package/dist/db/tokens.js +624 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/app-origin.js +26 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +84 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +216 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/app-host-literal.test.js +53 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +56 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +299 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +143 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +150 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +128 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/terminal-size.test.js +36 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +646 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +627 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/test/wrangler-route.test.js +27 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +213 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +154 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +104 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +243 -0
- package/docs/token-rotation.md +431 -0
- package/docs/view-urls.md +134 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +79 -27
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +53 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.travis.yml +0 -7
- package/lib/brane.cjs.js +0 -12
- package/lib/brane.cjs.js.map +0 -1
- package/lib/brane.d.ts +0 -11
- package/lib/brane.js +0 -6
- package/lib/brane.js.map +0 -1
- package/lib/tests.d.ts +0 -1
- package/lib/tests.js +0 -8
- package/lib/tests.js.map +0 -1
- package/rollup.config.js +0 -21
- package/scripts/test.sh +0 -8
- package/src/brane.ts +0 -21
- package/src/tests.ts +0 -8
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 膜 film's color schemes: which colors, in what order.
|
|
3
|
+
*
|
|
4
|
+
* The first palette (#465) was a cosine gradient whose three channel phases
|
|
5
|
+
* sat a third of a turn apart, so every scheme it could produce was the same
|
|
6
|
+
* rainbow — all hues, always in the same order — and the noise that walked it
|
|
7
|
+
* had a standard deviation of 0.097, too small to leave the rainbow. Across
|
|
8
|
+
* sixteen taps it read as about four schemes (claude-1cwe8u9p:22).
|
|
9
|
+
*
|
|
10
|
+
* A scheme here is two things, deliberately separate:
|
|
11
|
+
*
|
|
12
|
+
* - **Lightness** is today's banding, unchanged in character: a cosine in
|
|
13
|
+
* film thickness at 2.4–3.7 cycles per unit, swinging L from about 0.35 to
|
|
14
|
+
* 0.95. It is what gives the film its detail density and contrast, and the
|
|
15
|
+
* operator asked that those stay exactly as they were (claude-1cwe8u9p:28).
|
|
16
|
+
* - **Hue** comes from 3–5 anchor colors, randomly permuted — the same colors
|
|
17
|
+
* in another order are another film (claude-1cwe8u9p:23) — cycled through
|
|
18
|
+
* at their own rate, incommensurate with the lightness bands so the two
|
|
19
|
+
* never lock. Transitions between anchors are eased so a color holds and
|
|
20
|
+
* then turns, rather than smearing into its neighbor (claude-1cwe8u9p:29).
|
|
21
|
+
*
|
|
22
|
+
* No two anchors of one scheme may look alike (claude-1cwe8u9p:31–33). Hue
|
|
23
|
+
* angle alone does not guarantee that: where sRGB is narrow — the cyans — two
|
|
24
|
+
* hues 30° apart fit to nearly the same displayable color. So anchors are
|
|
25
|
+
* compared as they will render: gamut-fitted, in OKLab, at the lightnesses
|
|
26
|
+
* the bands pass through.
|
|
27
|
+
*
|
|
28
|
+
* Window-free, and every draw takes its random source as an argument, so
|
|
29
|
+
* `src/test/membrane-palette.test.ts` can pin all of this with a seeded one.
|
|
30
|
+
*/
|
|
31
|
+
export const MIN_ANCHORS = 3;
|
|
32
|
+
export const MAX_ANCHORS = 5;
|
|
33
|
+
/** Minimum hue separation between any two anchors, degrees. */
|
|
34
|
+
export const MIN_HUE_SEP_DEG = 28;
|
|
35
|
+
/** Minimum OKLab distance between any two anchors once fitted to sRGB. */
|
|
36
|
+
export const MIN_ANCHOR_DE = 0.09;
|
|
37
|
+
/** The lightnesses at which anchors are compared: the band's middle range. */
|
|
38
|
+
export const DISTINCT_AT_L = [0.5, 0.65, 0.8];
|
|
39
|
+
/** How far a keyframe must be from the one it replaces (mean OKLab a/b distance). */
|
|
40
|
+
export const MIN_SCHEME_DISTANCE = 0.12;
|
|
41
|
+
/**
|
|
42
|
+
* No color from the band between amber and green. The band from about 60°
|
|
43
|
+
* (amber) through yellow to about 130° (lime) reads as snot green and
|
|
44
|
+
* mustard at the lightnesses the film's bands pass through, and the
|
|
45
|
+
* operator wants it out of every scheme (claude-u6qlei0j:8, :17). Three
|
|
46
|
+
* measures:
|
|
47
|
+
*
|
|
48
|
+
* - No anchor in EXCLUDED_HUE_DEG. A draw that lands there is redrawn, not
|
|
49
|
+
* remapped, so no hue on either side is over-represented and the families
|
|
50
|
+
* keep their shape.
|
|
51
|
+
* - Anchors are joined by a straight line through OKLab a/b, not around the
|
|
52
|
+
* hue wheel (abAt, the shader's anchorAB). Around the wheel, green to
|
|
53
|
+
* magenta swept through yellow, orange and red — a small rainbow whose
|
|
54
|
+
* yellow strip was a mustard band with hard edges. Along the line the two
|
|
55
|
+
* fade through gray and pass through no hue they do not share. Neighboring
|
|
56
|
+
* hues (30–45°) look the same either way.
|
|
57
|
+
* - What still lands in the band — two anchors that straddle it closely,
|
|
58
|
+
* orange and green, say — is shown neutral: chroma fades to zero across
|
|
59
|
+
* BAND_FADE_DEG (ramped inside the band, so an anchor just outside keeps
|
|
60
|
+
* its color), and the film goes silvery there at the banding's lightness.
|
|
61
|
+
* (Before this the shader *lifted* lightness through the band so amber
|
|
62
|
+
* would glow, per claude-1cwe8u9p:38 — the opposite instinct.)
|
|
63
|
+
*/
|
|
64
|
+
export const EXCLUDED_HUE_DEG = [60, 130];
|
|
65
|
+
/** Chroma is faded to nothing between the inner two, ramping over the outer pairs. */
|
|
66
|
+
export const BAND_FADE_DEG = [60, 75, 115, 130];
|
|
67
|
+
/** Whether a hue (degrees, 0–360) is one no anchor may take. */
|
|
68
|
+
export function hueExcluded(hDeg) {
|
|
69
|
+
return hDeg >= EXCLUDED_HUE_DEG[0] && hDeg <= EXCLUDED_HUE_DEG[1];
|
|
70
|
+
}
|
|
71
|
+
/** How much of a hue's chroma the band takes away, 0 outside it, 1 in its middle. */
|
|
72
|
+
export function bandFade(hDeg) {
|
|
73
|
+
const [b0, b1, b2, b3] = BAND_FADE_DEG;
|
|
74
|
+
return smoothstep(b0, b1, hDeg) * (1 - smoothstep(b2, b3, hDeg));
|
|
75
|
+
}
|
|
76
|
+
/** An OKLab a/b offset with the band's chroma taken out of it. */
|
|
77
|
+
export function neutralizeBand(a, b) {
|
|
78
|
+
const hDeg = ((Math.atan2(b, a) * 180) / Math.PI + 360) % 360;
|
|
79
|
+
const k = 1 - bandFade(hDeg);
|
|
80
|
+
return [a * k, b * k];
|
|
81
|
+
}
|
|
82
|
+
// ---- OKLab (Björn Ottosson) ----
|
|
83
|
+
export function oklabToLinearSrgb(L, a, b) {
|
|
84
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
85
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
86
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
87
|
+
const l = l_ * l_ * l_, m = m_ * m_ * m_, s = s_ * s_ * s_;
|
|
88
|
+
return [
|
|
89
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
90
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
91
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
const EPS = 1e-4;
|
|
95
|
+
export function inSrgbGamut(L, a, b) {
|
|
96
|
+
return oklabToLinearSrgb(L, a, b).every((v) => v >= -EPS && v <= 1 + EPS);
|
|
97
|
+
}
|
|
98
|
+
/** OKLab of (L, C, h) with C reduced as far as needed to be displayable. The
|
|
99
|
+
* shader does the same fit per pixel, with fewer bisection steps. */
|
|
100
|
+
export function fitToGamut(L, C, h) {
|
|
101
|
+
const at = (c) => [L, c * Math.cos(h), c * Math.sin(h)];
|
|
102
|
+
if (inSrgbGamut(...at(C)))
|
|
103
|
+
return at(C);
|
|
104
|
+
let lo = 0, hi = C;
|
|
105
|
+
for (let i = 0; i < 20; i++) {
|
|
106
|
+
const mid = (lo + hi) / 2;
|
|
107
|
+
if (inSrgbGamut(...at(mid)))
|
|
108
|
+
lo = mid;
|
|
109
|
+
else
|
|
110
|
+
hi = mid;
|
|
111
|
+
}
|
|
112
|
+
return at(lo);
|
|
113
|
+
}
|
|
114
|
+
// ---- anchors ----
|
|
115
|
+
const DEG = Math.PI / 180;
|
|
116
|
+
const smoothstep = (a, b, x) => {
|
|
117
|
+
const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
|
|
118
|
+
return t * t * (3 - 2 * t);
|
|
119
|
+
};
|
|
120
|
+
export function hueSeparationDeg(a, b) {
|
|
121
|
+
const d = Math.abs((((a - b) % 360) + 360) % 360);
|
|
122
|
+
return Math.min(d, 360 - d);
|
|
123
|
+
}
|
|
124
|
+
/** Whether no two of these hues (degrees) would render alike. */
|
|
125
|
+
export function anchorsDistinct(huesDeg) {
|
|
126
|
+
for (let i = 0; i < huesDeg.length; i++) {
|
|
127
|
+
for (let j = i + 1; j < huesDeg.length; j++) {
|
|
128
|
+
if (hueSeparationDeg(huesDeg[i], huesDeg[j]) < MIN_HUE_SEP_DEG)
|
|
129
|
+
return false;
|
|
130
|
+
for (const L of DISTINCT_AT_L) {
|
|
131
|
+
const [, a1, b1] = fitToGamut(L, 0.3, huesDeg[i] * DEG);
|
|
132
|
+
const [, a2, b2] = fitToGamut(L, 0.3, huesDeg[j] * DEG);
|
|
133
|
+
if (Math.hypot(a1 - a2, b1 - b2) < MIN_ANCHOR_DE)
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
const range = (rng, lo, hi) => lo + (hi - lo) * rng();
|
|
141
|
+
const pick = (rng, xs) => xs[Math.floor(rng() * xs.length)];
|
|
142
|
+
/** Harmony families, each a base hue (degrees) → anchor hues (degrees). */
|
|
143
|
+
const FAMILIES = {
|
|
144
|
+
analogous: (h, rng) => {
|
|
145
|
+
const n = pick(rng, [3, 4]), s = range(rng, 30, 45);
|
|
146
|
+
return Array.from({ length: n }, (_, i) => h + s * (i - (n - 1) / 2));
|
|
147
|
+
},
|
|
148
|
+
complementary: (h, rng) => {
|
|
149
|
+
const s = range(rng, 35, 60);
|
|
150
|
+
return pick(rng, [[h, h + 180, h + s], [h, h + 180, h + 180 + s], [h, h + s, h + 180, h + 180 + s]]);
|
|
151
|
+
},
|
|
152
|
+
split: (h, rng) => { const s = range(rng, 25, 45); return [h, h + 180 - s, h + 180 + s]; },
|
|
153
|
+
triadic: (h) => [h, h + 120, h + 240],
|
|
154
|
+
tetradic: (h, rng) => { const w = pick(rng, [60, 90]); return [h, h + w, h + 180, h + 180 + w]; },
|
|
155
|
+
accent: (h, rng) => { const s = range(rng, 30, 45); return [h, h + s, h + pick(rng, [150, 180, 210]) + s / 2]; },
|
|
156
|
+
wild: (_h, rng) => Array.from({ length: pick(rng, [3, 4, 5]) }, () => range(rng, 0, 360)),
|
|
157
|
+
};
|
|
158
|
+
export const SCHEME_FAMILIES = Object.keys(FAMILIES);
|
|
159
|
+
function shuffle(xs, rng) {
|
|
160
|
+
for (let i = xs.length - 1; i > 0; i--) {
|
|
161
|
+
const j = Math.floor(rng() * (i + 1));
|
|
162
|
+
[xs[i], xs[j]] = [xs[j], xs[i]];
|
|
163
|
+
}
|
|
164
|
+
return xs;
|
|
165
|
+
}
|
|
166
|
+
/** One scheme: a family's hues, jittered, redrawn until all are distinct and
|
|
167
|
+
* none is in the excluded band, then permuted. */
|
|
168
|
+
export function sampleScheme(rng) {
|
|
169
|
+
let family, degs;
|
|
170
|
+
do {
|
|
171
|
+
family = pick(rng, SCHEME_FAMILIES);
|
|
172
|
+
degs = FAMILIES[family](range(rng, 0, 360), rng)
|
|
173
|
+
.map((h) => (((h + range(rng, -8, 8)) % 360) + 360) % 360);
|
|
174
|
+
} while (degs.length < MIN_ANCHORS || degs.length > MAX_ANCHORS || degs.some(hueExcluded) || !anchorsDistinct(degs));
|
|
175
|
+
const hues = shuffle(degs, rng).map((d) => d * DEG);
|
|
176
|
+
return {
|
|
177
|
+
family,
|
|
178
|
+
hues,
|
|
179
|
+
chromas: hues.map(() => range(rng, 0.15, 0.32)),
|
|
180
|
+
light: [range(rng, 2.4, 3.7), rng(), range(rng, 0.62, 0.68), range(rng, 0.27, 0.32)],
|
|
181
|
+
// At least one full pass through the anchors per unit thickness, so the
|
|
182
|
+
// glyph's range of thicknesses shows every one of them.
|
|
183
|
+
hue: [range(rng, 1.0, 1.5), rng(), range(rng, 0.18, 0.45)],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** The scheme's color at position `x` through one pass of the anchors,
|
|
187
|
+
* 0 ≤ x < 1, as an OKLab a/b offset — the shader's anchor lookup (anchorAB),
|
|
188
|
+
* for tests and tooling: each anchor holds, then turns into the next along
|
|
189
|
+
* a straight line, and the band is neutralized. */
|
|
190
|
+
export function abAt(s, x, w = s.hue[2]) {
|
|
191
|
+
const n = s.hues.length;
|
|
192
|
+
const pos = ((((x % 1) + 1) % 1) * n);
|
|
193
|
+
const i = Math.floor(pos) % n, j = (i + 1) % n;
|
|
194
|
+
const f = smoothstep(0.5 - w, 0.5 + w, pos - Math.floor(pos));
|
|
195
|
+
const ai = s.chromas[i] * Math.cos(s.hues[i]), bi = s.chromas[i] * Math.sin(s.hues[i]);
|
|
196
|
+
const aj = s.chromas[j] * Math.cos(s.hues[j]), bj = s.chromas[j] * Math.sin(s.hues[j]);
|
|
197
|
+
return neutralizeBand(ai + (aj - ai) * f, bi + (bj - bi) * f);
|
|
198
|
+
}
|
|
199
|
+
/** Hue (radians) and chroma at position `x`: abAt in polar form. */
|
|
200
|
+
export function hueChromaAt(s, x, w = s.hue[2]) {
|
|
201
|
+
const [a, b] = abAt(s, x, w);
|
|
202
|
+
return [Math.atan2(b, a), Math.hypot(a, b)];
|
|
203
|
+
}
|
|
204
|
+
/** Mean a/b distance between two schemes' hue cycles, pass for pass. Order
|
|
205
|
+
* matters: a permutation of the same anchors is far from the original. */
|
|
206
|
+
export function schemeDistance(p, q) {
|
|
207
|
+
const N = 32;
|
|
208
|
+
let t = 0;
|
|
209
|
+
for (let i = 0; i < N; i++) {
|
|
210
|
+
const [h1, c1] = hueChromaAt(p, i / N, 0.5);
|
|
211
|
+
const [h2, c2] = hueChromaAt(q, i / N, 0.5);
|
|
212
|
+
t += Math.hypot(c1 * Math.cos(h1) - c2 * Math.cos(h2), c1 * Math.sin(h1) - c2 * Math.sin(h2));
|
|
213
|
+
}
|
|
214
|
+
return t / N;
|
|
215
|
+
}
|
|
216
|
+
/** The keyframe after `prev`: a fresh scheme far enough from it — from each
|
|
217
|
+
* of them, when there are several, as a terrain cell's neighbors are — to
|
|
218
|
+
* read as a different film rather than a nudge. */
|
|
219
|
+
export function nextScheme(prev, rng) {
|
|
220
|
+
const prevs = prev === null ? [] : Array.isArray(prev) ? prev : [prev];
|
|
221
|
+
for (let tries = 0; tries < 200; tries++) {
|
|
222
|
+
const s = sampleScheme(rng);
|
|
223
|
+
if (prevs.every((p) => schemeDistance(s, p) >= MIN_SCHEME_DISTANCE))
|
|
224
|
+
return s;
|
|
225
|
+
}
|
|
226
|
+
return sampleScheme(rng);
|
|
227
|
+
}
|
|
228
|
+
/** Texels per scheme row in the shader's table (see packScheme). */
|
|
229
|
+
export const SCHEME_TEXELS = 5;
|
|
230
|
+
/**
|
|
231
|
+
* One scheme as a row of the shader's scheme table, SCHEME_TEXELS RGBA texels:
|
|
232
|
+
* (anchor count, hue rate, hue phase, transition half-width),
|
|
233
|
+
* (banding cycles, banding phase, L mid, L amplitude),
|
|
234
|
+
* then the anchors as packAnchors lays them out, two per texel.
|
|
235
|
+
*/
|
|
236
|
+
export function packScheme(s) {
|
|
237
|
+
const out = new Float32Array(SCHEME_TEXELS * 4);
|
|
238
|
+
out.set([s.hues.length, ...s.hue, ...s.light]);
|
|
239
|
+
out.set(packAnchors(s), 8);
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
/** Anchors packed for the shader: (hue, chroma) pairs, with the first repeated
|
|
243
|
+
* after the last so the lookup never needs a modulo, padded to MAX_ANCHORS + 1. */
|
|
244
|
+
export function packAnchors(s) {
|
|
245
|
+
const out = new Float32Array((MAX_ANCHORS + 1) * 2);
|
|
246
|
+
for (let k = 0; k <= MAX_ANCHORS; k++) {
|
|
247
|
+
const j = k % s.hues.length;
|
|
248
|
+
out[2 * k] = s.hues[j];
|
|
249
|
+
out[2 * k + 1] = s.chromas[j];
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=membrane-palette.js.map
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 膜 film's scheme terrain: which colors the base film wears *where*.
|
|
3
|
+
*
|
|
4
|
+
* Until this, the unblotched film had one scheme, the incumbent, and only a
|
|
5
|
+
* wash could change it. Now the scheme is a property of place: glyph space —
|
|
6
|
+
* the frame the splotches live in, the one a held drag shifts — is divided
|
|
7
|
+
* into cells, each with a scheme of its own. A hold-and-drag therefore
|
|
8
|
+
* scrolls the film through color space (claude-xaj5pb9b:3, :4): the base
|
|
9
|
+
* noise itself turns as it comes across a seam, and turns back when dragged
|
|
10
|
+
* back, because the map is fixed (claude-xaj5pb9b:5). The lift never scrolls
|
|
11
|
+
* it; splotches don't ride the lift either, so what is painted and what is
|
|
12
|
+
* found stay in one frame.
|
|
13
|
+
*
|
|
14
|
+
* **The cells are irregular** (claude-xaj5pb9b:16). The first cut tiled a
|
|
15
|
+
* square grid, and its axis-aligned seams were plain to see while scrolling
|
|
16
|
+
* — the eye locks onto straight lines moving together. Now each cell of the
|
|
17
|
+
* underlying grid has a *seed point* jittered within it, and a point of the
|
|
18
|
+
* film wears the scheme of the nearest seed (a Voronoi partition): walls at
|
|
19
|
+
* every angle, three-way corners, no two alike. Every cell whose seed is
|
|
20
|
+
* within SEAM of the nearest gets a share of the point, falling smoothly
|
|
21
|
+
* with the excess (`blend`), so the color is continuous everywhere — a
|
|
22
|
+
* nearest-two blend was not: it jumped wherever the runner-up changed,
|
|
23
|
+
* along a straight line inside every three-way corner. What the shares
|
|
24
|
+
* blend is the scheme's *parameters*, and the film's noise is rendered
|
|
25
|
+
* through the blended scheme (claude-xaj5pb9b:27) — blending the rendered
|
|
26
|
+
* colors instead averaged two bandings against each other and washed the
|
|
27
|
+
* texture out of every seam (claude-xaj5pb9b:26). And the point is
|
|
28
|
+
* looked up at a position displaced by a static multi-octave noise, so the
|
|
29
|
+
* walls bend on the scale of the glyph and are dithered on every smaller
|
|
30
|
+
* one (claude-xaj5pb9b:21). The grid is still there underneath as the key
|
|
31
|
+
* space — it is what makes a cell findable and its scheme stable — but
|
|
32
|
+
* nothing of it is visible.
|
|
33
|
+
*
|
|
34
|
+
* Three properties, each pinned by `src/test/membrane-terrain.test.ts`:
|
|
35
|
+
*
|
|
36
|
+
* - **Stable.** A cell's seed point and scheme are drawn from a generator
|
|
37
|
+
* seeded by (terrain seed, i, j) the first time the cell comes into reach,
|
|
38
|
+
* and kept. Returning to a cell shows what it showed before, for the life
|
|
39
|
+
* of the terrain (module scope in Membrane.web.tsx, so a rebuilt engine
|
|
40
|
+
* keeps it).
|
|
41
|
+
* - **Very different.** A cell's scheme is drawn with rejection until it is
|
|
42
|
+
* MIN_SCHEME_DISTANCE from every neighbor already drawn (the eight around
|
|
43
|
+
* it, since a jittered cell can share a wall with a diagonal one), the
|
|
44
|
+
* same rule `nextScheme` applies to one keyframe after another. Neighbors
|
|
45
|
+
* drawn later are far from it by the same rule, so any wall is a real
|
|
46
|
+
* change.
|
|
47
|
+
* - **Bounded.** The shader reads a BLOCK×BLOCK block of cells around the
|
|
48
|
+
* one under the glyph's centre (`blockRows`, row-major). A point's nearest
|
|
49
|
+
* seed lies in its own cell or one of the eight around it (JITTER keeps
|
|
50
|
+
* every seed well inside its cell), and every point the window, its seams
|
|
51
|
+
* and the seam warp can show lies within a cell of the centre, so a 5×5
|
|
52
|
+
* block always holds every row a pixel can ask for.
|
|
53
|
+
*
|
|
54
|
+
* A wash that wins paints its scheme onto the cells it covered
|
|
55
|
+
* (`paintCells`, `cellsInView`): the paint sticks to the map, so a wash is
|
|
56
|
+
* a place you can drag away from and back to. Seed points ride to the
|
|
57
|
+
* shader in the scheme table (`packCell`), so the CPU's idea of which cell
|
|
58
|
+
* a point is in and the shader's come from one set of numbers. Window-free.
|
|
59
|
+
*/
|
|
60
|
+
import { SCHEME_TEXELS, nextScheme, packScheme } from "./membrane-palette.js";
|
|
61
|
+
// ---- TUNING ----
|
|
62
|
+
/** Cell side, glyph widths: a drag of about this far shows a new scheme. */
|
|
63
|
+
export const CELL = 0.75;
|
|
64
|
+
/** How far a cell's seed point sits from its centre, at most, in cells,
|
|
65
|
+
* per axis. Below 0.5 it stays in its cell; the smaller, the squarer. */
|
|
66
|
+
export const JITTER = 0.35;
|
|
67
|
+
/** Blend across a wall: a cell whose seed is this much farther than the
|
|
68
|
+
* nearest, glyph widths, gets no share; nearer, a share falling smoothly
|
|
69
|
+
* from equal. A pixel a distance e from a wall differs by about 2e, so the
|
|
70
|
+
* visible seam is about SEAM wide. */
|
|
71
|
+
export const SEAM = 0.25;
|
|
72
|
+
/** How far the lookup point is displaced, glyph widths, by a multi-octave
|
|
73
|
+
* noise at this base frequency: the large octaves bend the walls, the
|
|
74
|
+
* small ones dither them. Typical, not maximum — see VIEW_MARGIN. */
|
|
75
|
+
export const WARP = 0.14;
|
|
76
|
+
export const WARP_FREQ = 1.5;
|
|
77
|
+
// ---- end TUNING ----
|
|
78
|
+
/** Cells on a side of the block the shader reads, and its row count. */
|
|
79
|
+
export const BLOCK = 5;
|
|
80
|
+
export const TERRAIN_ROWS = BLOCK * BLOCK;
|
|
81
|
+
/** Texels per row of the shader's scheme table: a scheme, then its cell's
|
|
82
|
+
* seed point (x, y, 0, 0) in world coordinates — zero for a group's row. */
|
|
83
|
+
export const TERRAIN_TEXELS = SCHEME_TEXELS + 1;
|
|
84
|
+
/** Beyond the window's edge, the farthest the warp can carry a lookup: which
|
|
85
|
+
* cells a point blends is decided at its warped position. The shader's fbm
|
|
86
|
+
* runs to about ±0.7 of its half-range, so ±1.4 × WARP at the extreme. */
|
|
87
|
+
export const VIEW_MARGIN = 1.5 * WARP;
|
|
88
|
+
export const createTerrain = (seed) => ({ seed, cells: new Map() });
|
|
89
|
+
const key = (i, j) => `${i},${j}`;
|
|
90
|
+
/** mulberry32: a small seeded generator, the one the tests use. */
|
|
91
|
+
export function seededRng(seed) {
|
|
92
|
+
let a = seed | 0;
|
|
93
|
+
return () => {
|
|
94
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
95
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
96
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
97
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** The generator for one cell: seed and coordinates hashed together. */
|
|
101
|
+
export function cellRng(seed, i, j) {
|
|
102
|
+
let h = (seed | 0) ^ Math.imul(i | 0, 0x9e3779b1) ^ Math.imul(j | 0, 0x85ebca77);
|
|
103
|
+
h = Math.imul(h ^ (h >>> 16), 0x7feb352d);
|
|
104
|
+
h = Math.imul(h ^ (h >>> 15), 0x846ca68b);
|
|
105
|
+
return seededRng(h ^ (h >>> 16));
|
|
106
|
+
}
|
|
107
|
+
/** The grid cell along one axis of a world coordinate (glyph widths). */
|
|
108
|
+
export const cellAt = (w) => Math.floor(w / CELL);
|
|
109
|
+
/** A screen point's world coordinate: the glyph centre is at world `-drag`,
|
|
110
|
+
* since a drag of `d` carried the film (and its map) by `d`. */
|
|
111
|
+
export const toWorld = (p, drag) => p - drag;
|
|
112
|
+
/** The grid cell under the glyph's centre after `drag` — the block's middle. */
|
|
113
|
+
export function centreCell(drag) {
|
|
114
|
+
return [cellAt(toWorld(0, drag[0])), cellAt(toWorld(0, drag[1]))];
|
|
115
|
+
}
|
|
116
|
+
const NEIGHBORS = [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]];
|
|
117
|
+
/** The cell, drawn on first reach: its seed point jittered from the grid
|
|
118
|
+
* cell's centre, its scheme far from every neighbor already drawn. */
|
|
119
|
+
export function cell(t, i, j) {
|
|
120
|
+
const k = key(i, j);
|
|
121
|
+
const have = t.cells.get(k);
|
|
122
|
+
if (have)
|
|
123
|
+
return have;
|
|
124
|
+
const rng = cellRng(t.seed, i, j);
|
|
125
|
+
const seed = [(i + 0.5 + (2 * rng() - 1) * JITTER) * CELL, (j + 0.5 + (2 * rng() - 1) * JITTER) * CELL];
|
|
126
|
+
const neighbors = NEIGHBORS.map(([di, dj]) => t.cells.get(key(i + di, j + dj))?.scheme)
|
|
127
|
+
.filter((s) => !!s);
|
|
128
|
+
const c = { seed, scheme: nextScheme(neighbors, rng) };
|
|
129
|
+
t.cells.set(k, c);
|
|
130
|
+
return c;
|
|
131
|
+
}
|
|
132
|
+
export const cellScheme = (t, i, j) => cell(t, i, j).scheme;
|
|
133
|
+
/** A cell's share of a world point, before normalizing: 1 for the nearest
|
|
134
|
+
* seed, falling smoothly to 0 for a seed SEAM farther than it. */
|
|
135
|
+
const share = (excess) => {
|
|
136
|
+
const t = Math.min(1, Math.max(0, excess / SEAM));
|
|
137
|
+
return 1 - t * t * (3 - 2 * t);
|
|
138
|
+
};
|
|
139
|
+
/** The cells a world point blends, by seed distance: every cell of the nine
|
|
140
|
+
* around its own whose seed is within SEAM of the nearest, with its share
|
|
141
|
+
* of the color, normalized — the shader's lookup, less its warp. The
|
|
142
|
+
* nearest always shares; at a wall two share equally; nothing jumps as the
|
|
143
|
+
* point moves, since every share is continuous in the distances. */
|
|
144
|
+
export function blend(t, w) {
|
|
145
|
+
const ci = cellAt(w[0]), cj = cellAt(w[1]);
|
|
146
|
+
const dists = [];
|
|
147
|
+
for (let dj = -1; dj <= 1; dj++) {
|
|
148
|
+
for (let di = -1; di <= 1; di++) {
|
|
149
|
+
const seed = cell(t, ci + di, cj + dj).seed;
|
|
150
|
+
dists.push({ cell: [ci + di, cj + dj], d: Math.hypot(w[0] - seed[0], w[1] - seed[1]) });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const d1 = Math.min(...dists.map((x) => x.d));
|
|
154
|
+
const shares = dists.map((x) => ({ cell: x.cell, weight: share(x.d - d1) })).filter((x) => x.weight > 0);
|
|
155
|
+
const total = shares.reduce((a, x) => a + x.weight, 0);
|
|
156
|
+
return shares.map((x) => ({ cell: x.cell, weight: x.weight / total })).sort((a, b) => b.weight - a.weight);
|
|
157
|
+
}
|
|
158
|
+
/** The block's cells around `centre`, row-major (y outer, x inner), as the
|
|
159
|
+
* shader's row order: row = (dj + R) * BLOCK + (di + R), R = (BLOCK - 1) / 2. */
|
|
160
|
+
export function blockCells(centre) {
|
|
161
|
+
const out = [];
|
|
162
|
+
const r = (BLOCK - 1) / 2;
|
|
163
|
+
for (let dj = -r; dj <= r; dj++)
|
|
164
|
+
for (let di = -r; di <= r; di++)
|
|
165
|
+
out.push([centre[0] + di, centre[1] + dj]);
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
/** The block's cells after `drag`, materializing what is new, in row order. */
|
|
169
|
+
export function blockRows(t, drag) {
|
|
170
|
+
return blockCells(centreCell(drag)).map(([i, j]) => cell(t, i, j));
|
|
171
|
+
}
|
|
172
|
+
/** Points of the window (glyph-normalized, -0.5..0.5) widened by VIEW_MARGIN,
|
|
173
|
+
* on a grid fine beside the seam, as world coordinates after `drag`. */
|
|
174
|
+
const VIEW_SAMPLES = 32;
|
|
175
|
+
function viewPoints(drag) {
|
|
176
|
+
const out = [];
|
|
177
|
+
const lo = -0.5 - VIEW_MARGIN, hi = 0.5 + VIEW_MARGIN;
|
|
178
|
+
for (let j = 0; j <= VIEW_SAMPLES; j++) {
|
|
179
|
+
for (let i = 0; i <= VIEW_SAMPLES; i++) {
|
|
180
|
+
out.push([toWorld(lo + ((hi - lo) * i) / VIEW_SAMPLES, drag[0]), toWorld(lo + ((hi - lo) * j) / VIEW_SAMPLES, drag[1])]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/** Every cell the window can show after `drag`: with a share of some point
|
|
186
|
+
* of the window widened by VIEW_MARGIN. Sorted, row-major. */
|
|
187
|
+
export function cellsInView(t, drag) {
|
|
188
|
+
const seen = new Map();
|
|
189
|
+
for (const w of viewPoints(drag))
|
|
190
|
+
for (const { cell: c } of blend(t, w))
|
|
191
|
+
seen.set(key(...c), c);
|
|
192
|
+
return [...seen.values()].sort((p, q) => p[1] - q[1] || p[0] - q[0]);
|
|
193
|
+
}
|
|
194
|
+
/** The schemes the window can show after `drag` — what a new color must be far from. */
|
|
195
|
+
export function schemesInView(t, drag) {
|
|
196
|
+
return cellsInView(t, drag).map(([i, j]) => cellScheme(t, i, j));
|
|
197
|
+
}
|
|
198
|
+
/** Paint `scheme` onto these cells, seed points kept: what a wash that won
|
|
199
|
+
* leaves on the map. */
|
|
200
|
+
export function paintCells(t, cells, scheme) {
|
|
201
|
+
for (const [i, j] of cells)
|
|
202
|
+
t.cells.set(key(i, j), { seed: cell(t, i, j).seed, scheme });
|
|
203
|
+
}
|
|
204
|
+
/** One cell as a row of the shader's scheme table: packScheme's texels, then
|
|
205
|
+
* the seed point. */
|
|
206
|
+
export function packCell(c) {
|
|
207
|
+
const out = new Float32Array(TERRAIN_TEXELS * 4);
|
|
208
|
+
out.set(packScheme(c.scheme));
|
|
209
|
+
out.set([c.seed[0], c.seed[1], 0, 0], SCHEME_TEXELS * 4);
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=membrane-terrain.js.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chat's message selection — which bubbles the operator has tapped.
|
|
3
|
+
*
|
|
4
|
+
* It lives outside React state on purpose. Held as `useState`, the selection
|
|
5
|
+
* made every tap a whole-screen event: a new `Set` re-rendered the `[nid]`
|
|
6
|
+
* screen, which rebuilt `handleTimestampPress`, which rebuilt `renderItem`,
|
|
7
|
+
* which handed every mounted `MessageBubble` a new callback prop and so missed
|
|
8
|
+
* `memo` on all of them. Two bubbles changed color; two hundred rows and the
|
|
9
|
+
* screen above them paid for it.
|
|
10
|
+
*
|
|
11
|
+
* So the store keeps its own listeners, and **per seq**: a toggle notifies the
|
|
12
|
+
* one row whose appearance changed, and a clear notifies exactly the rows that
|
|
13
|
+
* were selected — never a broadcast. The screen holds no selection state at
|
|
14
|
+
* all, which is what takes `renderItem` off the dependency treadmill.
|
|
15
|
+
*
|
|
16
|
+
* Pure, and in `shared/` rather than `expo/lib/` for the usual reason: the
|
|
17
|
+
* notification rules are the whole point and `src/test/message-selection.test.ts`
|
|
18
|
+
* can check them without a browser. The React binding — two
|
|
19
|
+
* `useSyncExternalStore` hooks — is the thin part, and lives in
|
|
20
|
+
* `expo/lib/selection.ts`.
|
|
21
|
+
*/
|
|
22
|
+
/** One stable empty array, so an untouched selection's snapshot keeps its
|
|
23
|
+
* identity and `useSyncExternalStore` never sees a spurious change. */
|
|
24
|
+
const NONE = Object.freeze([]);
|
|
25
|
+
export class MessageSelection {
|
|
26
|
+
selected = new Set();
|
|
27
|
+
/** Ascending, rebuilt only on change — `useSyncExternalStore` compares
|
|
28
|
+
* snapshots by identity and loops forever on a getter that allocates. */
|
|
29
|
+
snapshot = NONE;
|
|
30
|
+
perSeq = new Map();
|
|
31
|
+
whole = new Set();
|
|
32
|
+
/** Is this one selected? The question every bubble asks about itself. */
|
|
33
|
+
has = (seq) => this.selected.has(seq);
|
|
34
|
+
get size() {
|
|
35
|
+
return this.selected.size;
|
|
36
|
+
}
|
|
37
|
+
/** The selection, ascending — the `re` array for a signal, and the `seqs`
|
|
38
|
+
* an export is scoped to. Identity is stable between changes. */
|
|
39
|
+
getSelected = () => this.snapshot;
|
|
40
|
+
/** Watch the selection as a whole: the header's count, the composer's `re`.
|
|
41
|
+
* One component each, so re-rendering them on a tap is the point. */
|
|
42
|
+
subscribe = (listener) => {
|
|
43
|
+
this.whole.add(listener);
|
|
44
|
+
return () => { this.whole.delete(listener); };
|
|
45
|
+
};
|
|
46
|
+
/** Watch one row. This is the subscription that keeps a tap local. */
|
|
47
|
+
subscribeSeq = (seq, listener) => {
|
|
48
|
+
let set = this.perSeq.get(seq);
|
|
49
|
+
if (!set) {
|
|
50
|
+
set = new Set();
|
|
51
|
+
this.perSeq.set(seq, set);
|
|
52
|
+
}
|
|
53
|
+
set.add(listener);
|
|
54
|
+
return () => {
|
|
55
|
+
const current = this.perSeq.get(seq);
|
|
56
|
+
if (!current)
|
|
57
|
+
return;
|
|
58
|
+
current.delete(listener);
|
|
59
|
+
// Rows unmount as the list scrolls; an empty bucket left behind would
|
|
60
|
+
// make this map grow with every message ever scrolled past.
|
|
61
|
+
if (current.size === 0)
|
|
62
|
+
this.perSeq.delete(seq);
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
/** Select or deselect one. Returns whether it is now selected — the caller
|
|
66
|
+
* writes `?scrollToSeq=` from that, and a mutator that says what it did
|
|
67
|
+
* saves the caller a second read. */
|
|
68
|
+
toggle = (seq) => {
|
|
69
|
+
const selecting = !this.selected.has(seq);
|
|
70
|
+
if (selecting)
|
|
71
|
+
this.selected.add(seq);
|
|
72
|
+
else
|
|
73
|
+
this.selected.delete(seq);
|
|
74
|
+
this.commit([seq]);
|
|
75
|
+
return selecting;
|
|
76
|
+
};
|
|
77
|
+
/** Drop the whole selection. Returns what was dropped; notifies only those
|
|
78
|
+
* rows, because they are the only ones whose appearance changed. */
|
|
79
|
+
clear = () => {
|
|
80
|
+
if (this.selected.size === 0)
|
|
81
|
+
return [];
|
|
82
|
+
const cleared = [...this.selected];
|
|
83
|
+
this.selected.clear();
|
|
84
|
+
this.commit(cleared);
|
|
85
|
+
return cleared;
|
|
86
|
+
};
|
|
87
|
+
/** Make the selection exactly this — used when a `?scrollToSeq=` arrives
|
|
88
|
+
* with the route. Notifies the symmetric difference and nothing else. */
|
|
89
|
+
replace = (seqs) => {
|
|
90
|
+
const next = new Set();
|
|
91
|
+
for (const seq of seqs)
|
|
92
|
+
if (Number.isInteger(seq) && seq > 0)
|
|
93
|
+
next.add(seq);
|
|
94
|
+
const changed = [];
|
|
95
|
+
for (const seq of this.selected)
|
|
96
|
+
if (!next.has(seq))
|
|
97
|
+
changed.push(seq);
|
|
98
|
+
for (const seq of next)
|
|
99
|
+
if (!this.selected.has(seq))
|
|
100
|
+
changed.push(seq);
|
|
101
|
+
if (changed.length === 0)
|
|
102
|
+
return;
|
|
103
|
+
this.selected = next;
|
|
104
|
+
this.commit(changed);
|
|
105
|
+
};
|
|
106
|
+
/** Rebuild the snapshot, then notify — in that order, so a listener that
|
|
107
|
+
* reads `getSelected()` during its own notification sees the new value
|
|
108
|
+
* rather than the one it was woken about. */
|
|
109
|
+
commit(changed) {
|
|
110
|
+
this.snapshot = this.selected.size === 0
|
|
111
|
+
? NONE
|
|
112
|
+
: [...this.selected].sort((a, b) => a - b);
|
|
113
|
+
for (const seq of changed) {
|
|
114
|
+
const listeners = this.perSeq.get(seq);
|
|
115
|
+
if (listeners)
|
|
116
|
+
for (const listener of [...listeners])
|
|
117
|
+
listener();
|
|
118
|
+
}
|
|
119
|
+
for (const listener of [...this.whole])
|
|
120
|
+
listener();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=message-selection.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 膜 (mó, "membrane") outline that brane wears where it has nothing else to
|
|
3
|
+
* show — the list screen's empty state — and that `mo-membrane.svg` is cut
|
|
4
|
+
* from. One glyph, one source: the app draws this with `Path2D`, so it needs no
|
|
5
|
+
* web font, no network, and renders the same in the offline PWA as in the SVG.
|
|
6
|
+
*
|
|
7
|
+
* Provenance: Noto Serif SC Black (Google/Adobe Source Han Serif, weight 900),
|
|
8
|
+
* taken from Google Fonts' one-character subset
|
|
9
|
+
* (`css2?family=Noto+Serif+SC:wght@900&text=膜`, 1.9 KB TrueType) and read
|
|
10
|
+
* with opentype.js: `font.charToGlyph("膜").path` in font units (1000/em,
|
|
11
|
+
* y up), then scaled by 0.86 and centered in a `MO_GLYPH_BOX`-unit square
|
|
12
|
+
* with y flipped, so the coordinates below are already SVG-shaped. Noto fonts
|
|
13
|
+
* are OFL-licensed; the outline may be embedded.
|
|
14
|
+
*
|
|
15
|
+
* Window-free, like every module here, so `src/test/mo-glyph.test.ts` can pin
|
|
16
|
+
* that the data is a closed path inside its box.
|
|
17
|
+
*/
|
|
18
|
+
/** Side of the square the path is laid out in, in path units. */
|
|
19
|
+
export const MO_GLYPH_BOX = 1000;
|
|
20
|
+
/** SVG path data for 膜, y down, inside `[0, MO_GLYPH_BOX]²`. */
|
|
21
|
+
export const MO_GLYPH_PATH = "M493.98 449.69L493.98 425.61L791.54 425.61L791.54 449.69L493.98 449.69ZM493.98 557.19L493.98 533.11" +
|
|
22
|
+
"L791.54 533.11L791.54 557.19L493.98 557.19ZM431.20 606.21L431.20 319.83L431.20 275.11L431.20 275.11" +
|
|
23
|
+
"L547.30 319.83L780.36 319.83L780.36 343.91L543.00 343.91L543.00 571.81Q543.00 576.11 528.81 584.71" +
|
|
24
|
+
"Q514.62 593.31 492.26 599.76Q469.90 606.21 446.68 606.21L446.68 606.21L431.20 606.21ZM733.92 583.85" +
|
|
25
|
+
"L733.92 319.83L726.18 319.83L776.92 266.51L878.40 343.05Q875.82 346.49 869.37 350.79" +
|
|
26
|
+
"Q862.92 355.09 851.74 357.67L851.74 357.67L851.74 552.03Q851.74 555.47 835.83 563.21" +
|
|
27
|
+
"Q819.92 570.95 797.56 577.40Q775.20 583.85 754.56 583.85L754.56 583.85L733.92 583.85ZM400.24 674.15" +
|
|
28
|
+
"L393.36 650.07L757.14 650.07L812.18 578.69Q812.18 578.69 829.38 592.02Q846.58 605.35 869.80 624.27" +
|
|
29
|
+
"Q893.02 643.19 911.08 660.39L911.08 660.39Q907.64 674.15 887.00 674.15L887.00 674.15L400.24 674.15Z" +
|
|
30
|
+
"M675.44 656.95L685.76 653.51Q694.36 680.17 723.17 703.82Q751.98 727.47 799.28 745.10" +
|
|
31
|
+
"Q846.58 762.73 911.94 773.05L911.94 773.05L911.94 783.37Q867.22 795.41 844.86 821.64" +
|
|
32
|
+
"Q822.50 847.87 820.78 904.63L820.78 904.63Q785.52 887.43 760.15 858.19Q734.78 828.95 717.58 793.69" +
|
|
33
|
+
"Q700.38 758.43 690.06 722.74Q679.74 687.05 675.44 656.95L675.44 656.95ZM132.78 417.01L132.78 148.69" +
|
|
34
|
+
"L132.78 140.09L132.78 104.83L259.20 148.69L242.86 148.69L242.86 417.01Q242.86 475.49 239.85 539.56" +
|
|
35
|
+
"Q236.84 603.63 223.94 668.56Q211.04 733.49 180.94 794.12Q150.84 854.75 96.66 906.35L96.66 906.35" +
|
|
36
|
+
"L87.20 901.19Q111.28 829.81 120.74 748.54Q130.20 667.27 131.49 582.13Q132.78 496.99 132.78 417.01" +
|
|
37
|
+
"L132.78 417.01ZM184.38 172.77L184.38 148.69L342.62 148.69L342.62 172.77L184.38 172.77ZM184.38 371.43" +
|
|
38
|
+
"L184.38 347.35L342.62 347.35L342.62 371.43L184.38 371.43ZM184.38 573.53L184.38 549.45L342.62 549.45" +
|
|
39
|
+
"L342.62 573.53L184.38 573.53ZM284.14 767.89L284.14 148.69L275.54 148.69L325.42 92.79L426.90 171.91" +
|
|
40
|
+
"Q423.46 176.21 415.72 180.94Q407.98 185.67 395.94 188.25L395.94 188.25L395.94 779.93" +
|
|
41
|
+
"Q395.94 817.77 387.77 844.43Q379.60 871.09 352.51 886.57Q325.42 902.05 269.52 907.21L269.52 907.21" +
|
|
42
|
+
"Q268.66 879.69 266.08 859.05Q263.50 838.41 256.62 827.23L256.62 827.23Q250.60 814.33 239.42 805.30" +
|
|
43
|
+
"Q228.24 796.27 205.02 791.97L205.02 791.97L205.02 779.93Q205.02 779.93 213.19 780.36" +
|
|
44
|
+
"Q221.36 780.79 232.54 781.65Q243.72 782.51 254.04 782.94Q264.36 783.37 269.52 783.37L269.52 783.37" +
|
|
45
|
+
"Q278.98 783.37 281.56 779.07Q284.14 774.77 284.14 767.89L284.14 767.89ZM499.14 291.45L499.14 99.67" +
|
|
46
|
+
"L639.32 110.85Q638.46 118.59 632.87 124.18Q627.28 129.77 610.94 132.35L610.94 132.35L610.94 265.65" +
|
|
47
|
+
"Q610.94 269.09 596.32 275.54Q581.70 281.99 560.20 286.72Q538.70 291.45 516.34 291.45L516.34 291.45" +
|
|
48
|
+
"L499.14 291.45ZM661.68 291.45L661.68 99.67L801.86 110.85Q801.00 118.59 795.41 124.18" +
|
|
49
|
+
"Q789.82 129.77 774.34 132.35L774.34 132.35L774.34 265.65Q774.34 269.09 759.72 275.54" +
|
|
50
|
+
"Q745.10 281.99 723.17 286.72Q701.24 291.45 678.88 291.45L678.88 291.45L661.68 291.45ZM407.98 219.21" +
|
|
51
|
+
"L401.10 195.13L797.56 195.13L838.84 133.21Q838.84 133.21 851.74 145.25Q864.64 157.29 882.27 174.06" +
|
|
52
|
+
"Q899.90 190.83 912.80 205.45L912.80 205.45Q910.22 219.21 890.44 219.21L890.44 219.21L407.98 219.21Z" +
|
|
53
|
+
"M573.10 533.11L573.10 533.11L695.22 533.11Q692.64 582.99 686.19 628.57Q679.74 674.15 662.54 714.57" +
|
|
54
|
+
"Q645.34 754.99 611.37 789.82Q577.40 824.65 521.07 853.89Q464.74 883.13 379.60 906.35L379.60 906.35" +
|
|
55
|
+
"L372.72 895.17Q447.54 858.19 488.39 817.77Q529.24 777.35 546.87 732.20Q564.50 687.05 568.37 637.60" +
|
|
56
|
+
"Q572.24 588.15 573.10 533.11Z";
|
|
57
|
+
//# sourceMappingURL=mo-glyph.js.map
|