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.
Files changed (352) hide show
  1. package/AGENTS.md +367 -0
  2. package/CHANGELOG.md +66 -0
  3. package/CONTRIBUTING.md +101 -0
  4. package/DESIGN.md +405 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +213 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +133 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +360 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1101 -0
  37. package/dist/commands/token.js +154 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6353 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +643 -0
  58. package/dist/db/tokens.js +624 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/app-origin.js +26 -0
  96. package/dist/shared/block-glyphs.js +114 -0
  97. package/dist/shared/diagram-css.js +17 -0
  98. package/dist/shared/export-selection.js +101 -0
  99. package/dist/shared/find-in-text.js +110 -0
  100. package/dist/shared/glow-hues.js +225 -0
  101. package/dist/shared/hex-dump.js +62 -0
  102. package/dist/shared/hex-pages.js +52 -0
  103. package/dist/shared/hit-drum.js +79 -0
  104. package/dist/shared/known-nids.js +58 -0
  105. package/dist/shared/list-scroll-keys.js +56 -0
  106. package/dist/shared/markdown-lists.js +72 -0
  107. package/dist/shared/markdown-math.js +485 -0
  108. package/dist/shared/markdown-mentions.js +79 -0
  109. package/dist/shared/markdown-nids.js +41 -0
  110. package/dist/shared/markdown-protocols.js +53 -0
  111. package/dist/shared/markdown-refs.js +123 -0
  112. package/dist/shared/membrane-invasion.js +342 -0
  113. package/dist/shared/membrane-motion.js +83 -0
  114. package/dist/shared/membrane-palette.js +253 -0
  115. package/dist/shared/membrane-terrain.js +212 -0
  116. package/dist/shared/message-selection.js +123 -0
  117. package/dist/shared/mo-glyph.js +57 -0
  118. package/dist/shared/narrative-format.js +237 -0
  119. package/dist/shared/narrative-menu.js +84 -0
  120. package/dist/shared/narrative-sort.js +47 -0
  121. package/dist/shared/narrative-status.js +52 -0
  122. package/dist/shared/no-pan.js +85 -0
  123. package/dist/shared/pane-layout.js +50 -0
  124. package/dist/shared/payload-inline.js +83 -0
  125. package/dist/shared/speaker-color.js +61 -0
  126. package/dist/shared/token-rotation.js +14 -0
  127. package/dist/shared/view-url.js +216 -0
  128. package/dist/shared/wiki-links.js +70 -0
  129. package/dist/shared/wiki-window.js +4 -0
  130. package/dist/tailscale.js +55 -0
  131. package/dist/test/absorb-claude.test.js +892 -0
  132. package/dist/test/agent-scope.test.js +193 -0
  133. package/dist/test/agents.test.js +55 -0
  134. package/dist/test/api-request.test.js +122 -0
  135. package/dist/test/app-host-literal.test.js +53 -0
  136. package/dist/test/awaiting-glow.test.js +54 -0
  137. package/dist/test/awaiting.test.js +122 -0
  138. package/dist/test/bind-token.test.js +88 -0
  139. package/dist/test/block-glyphs.test.js +68 -0
  140. package/dist/test/cert-manager.test.js +262 -0
  141. package/dist/test/claude-live-session.test.js +110 -0
  142. package/dist/test/codex-tool-log.test.js +568 -0
  143. package/dist/test/config.test.js +71 -0
  144. package/dist/test/copilot-tool-log.test.js +93 -0
  145. package/dist/test/cors-preflight.test.js +56 -0
  146. package/dist/test/cost-linger.test.js +31 -0
  147. package/dist/test/cost-pricing.test.js +72 -0
  148. package/dist/test/cost-reprice.test.js +139 -0
  149. package/dist/test/cost-sessions.test.js +301 -0
  150. package/dist/test/crush-tool-log.test.js +289 -0
  151. package/dist/test/daemon-client.test.js +109 -0
  152. package/dist/test/daemon-socket.test.js +293 -0
  153. package/dist/test/diagram-arrows.test.js +205 -0
  154. package/dist/test/diagram-export.test.js +210 -0
  155. package/dist/test/diagram-labels.test.js +313 -0
  156. package/dist/test/diagram-parse.test.js +191 -0
  157. package/dist/test/diagram-render.test.js +175 -0
  158. package/dist/test/diagram-review.test.js +288 -0
  159. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  160. package/dist/test/embedder.test.js +84 -0
  161. package/dist/test/events.test.js +123 -0
  162. package/dist/test/export-selection.test.js +102 -0
  163. package/dist/test/export.test.js +505 -0
  164. package/dist/test/files-browse.test.js +393 -0
  165. package/dist/test/files-find.test.js +272 -0
  166. package/dist/test/find-in-text.test.js +86 -0
  167. package/dist/test/first-paint-viewport.test.js +59 -0
  168. package/dist/test/fixtures/embedding-golden.js +23 -0
  169. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  170. package/dist/test/glow-hues.test.js +202 -0
  171. package/dist/test/harness-cost.test.js +423 -0
  172. package/dist/test/helpers.js +21 -0
  173. package/dist/test/hex-pages.test.js +68 -0
  174. package/dist/test/highlight.test.js +69 -0
  175. package/dist/test/hit-drum.test.js +71 -0
  176. package/dist/test/hook-config.test.js +299 -0
  177. package/dist/test/json-gzip.test.js +70 -0
  178. package/dist/test/key-routing.test.js +58 -0
  179. package/dist/test/known-nids.test.js +106 -0
  180. package/dist/test/link-preview.test.js +67 -0
  181. package/dist/test/list-scroll-keys.test.js +66 -0
  182. package/dist/test/listen-own-words.test.js +147 -0
  183. package/dist/test/listen-timeout.test.js +33 -0
  184. package/dist/test/markdown-diagnostics.test.js +121 -0
  185. package/dist/test/markdown-lists.test.js +182 -0
  186. package/dist/test/markdown-math.test.js +256 -0
  187. package/dist/test/markdown-nids.test.js +146 -0
  188. package/dist/test/markdown-protocols.test.js +139 -0
  189. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  190. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  191. package/dist/test/mcp-narrative.test.js +193 -0
  192. package/dist/test/mcp.test.js +548 -0
  193. package/dist/test/meld.test.js +143 -0
  194. package/dist/test/membrane-invasion.test.js +482 -0
  195. package/dist/test/membrane-motion.test.js +101 -0
  196. package/dist/test/membrane-palette.test.js +166 -0
  197. package/dist/test/membrane-terrain.test.js +170 -0
  198. package/dist/test/memory-log.test.js +202 -0
  199. package/dist/test/mentions-e2e.test.js +261 -0
  200. package/dist/test/mentions-ledger.test.js +265 -0
  201. package/dist/test/mentions-listen.test.js +265 -0
  202. package/dist/test/mentions.test.js +238 -0
  203. package/dist/test/message-selection.test.js +114 -0
  204. package/dist/test/mo-glyph.test.js +22 -0
  205. package/dist/test/model-resolution.test.js +166 -0
  206. package/dist/test/narrate-validate.test.js +77 -0
  207. package/dist/test/narrate-wait.test.js +160 -0
  208. package/dist/test/narrative-format.test.js +219 -0
  209. package/dist/test/narrative-menu.test.js +150 -0
  210. package/dist/test/narrative-sort.test.js +75 -0
  211. package/dist/test/no-pan.test.js +60 -0
  212. package/dist/test/pane-layout.test.js +153 -0
  213. package/dist/test/paths.test.js +37 -0
  214. package/dist/test/payload-inline.test.js +91 -0
  215. package/dist/test/precache-fonts.test.js +72 -0
  216. package/dist/test/presence-gate.test.js +440 -0
  217. package/dist/test/presence.test.js +160 -0
  218. package/dist/test/read-events.test.js +220 -0
  219. package/dist/test/remote-meld.test.js +128 -0
  220. package/dist/test/revive-cli.test.js +93 -0
  221. package/dist/test/search-floor.test.js +206 -0
  222. package/dist/test/search-fusion.test.js +168 -0
  223. package/dist/test/search.test.js +205 -0
  224. package/dist/test/security.test.js +48 -0
  225. package/dist/test/selection-wiring.test.js +61 -0
  226. package/dist/test/session-affinity.test.js +295 -0
  227. package/dist/test/signal-source.test.js +32 -0
  228. package/dist/test/socket-account.test.js +268 -0
  229. package/dist/test/spawn-toml.test.js +72 -0
  230. package/dist/test/speaker-color.test.js +90 -0
  231. package/dist/test/tailscale-rebind.test.js +79 -0
  232. package/dist/test/terminal-cursor.test.js +61 -0
  233. package/dist/test/terminal-resync.test.js +23 -0
  234. package/dist/test/terminal-size.test.js +36 -0
  235. package/dist/test/thin-cli.test.js +382 -0
  236. package/dist/test/token-bootstrap.test.js +272 -0
  237. package/dist/test/token-devices.test.js +646 -0
  238. package/dist/test/token-lapse.test.js +94 -0
  239. package/dist/test/token-ring.test.js +188 -0
  240. package/dist/test/token-rotate-cli.test.js +58 -0
  241. package/dist/test/token-routes.test.js +95 -0
  242. package/dist/test/tool-uses.test.js +1006 -0
  243. package/dist/test/usage-ledger.test.js +126 -0
  244. package/dist/test/view-url.test.js +627 -0
  245. package/dist/test/wiki-links.test.js +57 -0
  246. package/dist/test/wiki-sources.test.js +85 -0
  247. package/dist/test/wiki-window.test.js +31 -0
  248. package/dist/test/wiki.test.js +399 -0
  249. package/dist/test/wordpiece.test.js +68 -0
  250. package/dist/test/wrangler-route.test.js +27 -0
  251. package/dist/tls.js +148 -0
  252. package/dist/tmux.js +213 -0
  253. package/dist/user-agent.js +46 -0
  254. package/docker-entrypoint.sh +154 -0
  255. package/docs/README.md +3 -0
  256. package/docs/absorb.md +231 -0
  257. package/docs/cost-ledger.md +125 -0
  258. package/docs/daemon-api.md +189 -0
  259. package/docs/diagrams.md +163 -0
  260. package/docs/doctor.md +36 -0
  261. package/docs/export-scope.md +69 -0
  262. package/docs/files-view.md +133 -0
  263. package/docs/link-previews.md +38 -0
  264. package/docs/math.md +32 -0
  265. package/docs/mcp.md +69 -0
  266. package/docs/memory-search.md +75 -0
  267. package/docs/mentions.md +101 -0
  268. package/docs/multi-brane.md +14 -0
  269. package/docs/passkeys.md +165 -0
  270. package/docs/presence.md +202 -0
  271. package/docs/releasing.md +104 -0
  272. package/docs/rendering-warnings.md +22 -0
  273. package/docs/request-layer.md +72 -0
  274. package/docs/selection-store.md +50 -0
  275. package/docs/session-affinity.md +61 -0
  276. package/docs/terminal-mirroring.md +243 -0
  277. package/docs/token-rotation.md +431 -0
  278. package/docs/view-urls.md +134 -0
  279. package/docs/web-app.md +23 -0
  280. package/docs/wiki.md +43 -0
  281. package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
  282. package/expo/dist/apple-touch-icon.png +0 -0
  283. package/expo/dist/favicon.svg +3 -0
  284. package/expo/dist/index.html +177 -0
  285. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  301. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  302. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  303. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  304. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  305. package/expo/dist/katex/katex.min.css +1 -0
  306. package/expo/dist/manifest.json +16 -0
  307. package/expo/dist/metadata.json +1 -0
  308. package/expo/dist/og/membrane.v1.png +0 -0
  309. package/expo/dist/pdf.min.mjs +28 -0
  310. package/expo/dist/pdf.worker.min.mjs +28 -0
  311. package/expo/dist/sw.js +154 -0
  312. package/package.json +79 -27
  313. package/protocols/LIVE_loop.md +407 -0
  314. package/protocols/bebrief.md +41 -0
  315. package/protocols/digest.md +231 -0
  316. package/protocols/fly.md +266 -0
  317. package/protocols/idle.md +81 -0
  318. package/protocols/keeper.md +829 -0
  319. package/protocols/markdown.md +80 -0
  320. package/protocols/memory.md +419 -0
  321. package/protocols/mentions.md +93 -0
  322. package/protocols/payloads.md +53 -0
  323. package/protocols/prelude.md +91 -0
  324. package/protocols/ramify.md +105 -0
  325. package/protocols/review.md +356 -0
  326. package/protocols/rrr.md +232 -0
  327. package/protocols/scribe.md +429 -0
  328. package/protocols/summarize.md +203 -0
  329. package/protocols/twin.md +174 -0
  330. package/protocols/wiki.md +438 -0
  331. package/protocols/worker.md +38 -0
  332. package/wiki/shared/append-only-event-log.md +148 -0
  333. package/wiki/shared/index.md +53 -0
  334. package/wiki/shared/owning-your-narratives.md +161 -0
  335. package/wiki/shared/premature-ontologies.md +435 -0
  336. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  337. package/wiki/shared/sequestering-the-narrative.md +143 -0
  338. package/wiki/shared/why-wikis-work.md +216 -0
  339. package/.travis.yml +0 -7
  340. package/lib/brane.cjs.js +0 -12
  341. package/lib/brane.cjs.js.map +0 -1
  342. package/lib/brane.d.ts +0 -11
  343. package/lib/brane.js +0 -6
  344. package/lib/brane.js.map +0 -1
  345. package/lib/tests.d.ts +0 -1
  346. package/lib/tests.js +0 -8
  347. package/lib/tests.js.map +0 -1
  348. package/rollup.config.js +0 -21
  349. package/scripts/test.sh +0 -8
  350. package/src/brane.ts +0 -21
  351. package/src/tests.ts +0 -8
  352. 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