brane 0.0.2 → 0.3.0

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 (338) hide show
  1. package/AGENTS.md +362 -0
  2. package/CHANGELOG.md +33 -0
  3. package/CONTRIBUTING.md +91 -0
  4. package/DESIGN.md +402 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +205 -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 +116 -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 +332 -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 +1094 -0
  37. package/dist/commands/token.js +153 -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 +6349 -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 +633 -0
  58. package/dist/db/tokens.js +583 -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/block-glyphs.js +114 -0
  96. package/dist/shared/diagram-css.js +17 -0
  97. package/dist/shared/export-selection.js +101 -0
  98. package/dist/shared/find-in-text.js +110 -0
  99. package/dist/shared/glow-hues.js +225 -0
  100. package/dist/shared/hex-dump.js +62 -0
  101. package/dist/shared/hex-pages.js +52 -0
  102. package/dist/shared/hit-drum.js +79 -0
  103. package/dist/shared/known-nids.js +58 -0
  104. package/dist/shared/list-scroll-keys.js +56 -0
  105. package/dist/shared/markdown-lists.js +72 -0
  106. package/dist/shared/markdown-math.js +485 -0
  107. package/dist/shared/markdown-mentions.js +79 -0
  108. package/dist/shared/markdown-nids.js +41 -0
  109. package/dist/shared/markdown-protocols.js +53 -0
  110. package/dist/shared/markdown-refs.js +123 -0
  111. package/dist/shared/membrane-invasion.js +342 -0
  112. package/dist/shared/membrane-motion.js +83 -0
  113. package/dist/shared/membrane-palette.js +253 -0
  114. package/dist/shared/membrane-terrain.js +212 -0
  115. package/dist/shared/message-selection.js +123 -0
  116. package/dist/shared/mo-glyph.js +57 -0
  117. package/dist/shared/narrative-format.js +237 -0
  118. package/dist/shared/narrative-menu.js +82 -0
  119. package/dist/shared/narrative-sort.js +47 -0
  120. package/dist/shared/narrative-status.js +52 -0
  121. package/dist/shared/no-pan.js +85 -0
  122. package/dist/shared/pane-layout.js +50 -0
  123. package/dist/shared/payload-inline.js +83 -0
  124. package/dist/shared/speaker-color.js +61 -0
  125. package/dist/shared/token-rotation.js +14 -0
  126. package/dist/shared/view-url.js +197 -0
  127. package/dist/shared/wiki-links.js +70 -0
  128. package/dist/shared/wiki-window.js +4 -0
  129. package/dist/tailscale.js +55 -0
  130. package/dist/test/absorb-claude.test.js +892 -0
  131. package/dist/test/agent-scope.test.js +193 -0
  132. package/dist/test/agents.test.js +55 -0
  133. package/dist/test/api-request.test.js +122 -0
  134. package/dist/test/awaiting-glow.test.js +54 -0
  135. package/dist/test/awaiting.test.js +122 -0
  136. package/dist/test/bind-token.test.js +88 -0
  137. package/dist/test/block-glyphs.test.js +68 -0
  138. package/dist/test/cert-manager.test.js +262 -0
  139. package/dist/test/claude-live-session.test.js +110 -0
  140. package/dist/test/codex-tool-log.test.js +568 -0
  141. package/dist/test/config.test.js +71 -0
  142. package/dist/test/copilot-tool-log.test.js +93 -0
  143. package/dist/test/cors-preflight.test.js +49 -0
  144. package/dist/test/cost-linger.test.js +31 -0
  145. package/dist/test/cost-pricing.test.js +72 -0
  146. package/dist/test/cost-reprice.test.js +139 -0
  147. package/dist/test/cost-sessions.test.js +301 -0
  148. package/dist/test/crush-tool-log.test.js +289 -0
  149. package/dist/test/daemon-client.test.js +109 -0
  150. package/dist/test/daemon-socket.test.js +293 -0
  151. package/dist/test/diagram-arrows.test.js +205 -0
  152. package/dist/test/diagram-export.test.js +210 -0
  153. package/dist/test/diagram-labels.test.js +313 -0
  154. package/dist/test/diagram-parse.test.js +191 -0
  155. package/dist/test/diagram-render.test.js +175 -0
  156. package/dist/test/diagram-review.test.js +288 -0
  157. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  158. package/dist/test/embedder.test.js +84 -0
  159. package/dist/test/events.test.js +123 -0
  160. package/dist/test/export-selection.test.js +102 -0
  161. package/dist/test/export.test.js +505 -0
  162. package/dist/test/files-browse.test.js +393 -0
  163. package/dist/test/files-find.test.js +272 -0
  164. package/dist/test/find-in-text.test.js +86 -0
  165. package/dist/test/first-paint-viewport.test.js +59 -0
  166. package/dist/test/fixtures/embedding-golden.js +23 -0
  167. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  168. package/dist/test/glow-hues.test.js +202 -0
  169. package/dist/test/harness-cost.test.js +423 -0
  170. package/dist/test/helpers.js +21 -0
  171. package/dist/test/hex-pages.test.js +68 -0
  172. package/dist/test/highlight.test.js +69 -0
  173. package/dist/test/hit-drum.test.js +71 -0
  174. package/dist/test/hook-config.test.js +262 -0
  175. package/dist/test/json-gzip.test.js +70 -0
  176. package/dist/test/key-routing.test.js +58 -0
  177. package/dist/test/known-nids.test.js +106 -0
  178. package/dist/test/link-preview.test.js +67 -0
  179. package/dist/test/list-scroll-keys.test.js +66 -0
  180. package/dist/test/listen-own-words.test.js +147 -0
  181. package/dist/test/listen-timeout.test.js +33 -0
  182. package/dist/test/markdown-diagnostics.test.js +121 -0
  183. package/dist/test/markdown-lists.test.js +182 -0
  184. package/dist/test/markdown-math.test.js +256 -0
  185. package/dist/test/markdown-nids.test.js +146 -0
  186. package/dist/test/markdown-protocols.test.js +139 -0
  187. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  188. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  189. package/dist/test/mcp-narrative.test.js +193 -0
  190. package/dist/test/mcp.test.js +548 -0
  191. package/dist/test/meld.test.js +85 -0
  192. package/dist/test/membrane-invasion.test.js +482 -0
  193. package/dist/test/membrane-motion.test.js +101 -0
  194. package/dist/test/membrane-palette.test.js +166 -0
  195. package/dist/test/membrane-terrain.test.js +170 -0
  196. package/dist/test/memory-log.test.js +202 -0
  197. package/dist/test/mentions-e2e.test.js +261 -0
  198. package/dist/test/mentions-ledger.test.js +265 -0
  199. package/dist/test/mentions-listen.test.js +265 -0
  200. package/dist/test/mentions.test.js +238 -0
  201. package/dist/test/message-selection.test.js +114 -0
  202. package/dist/test/mo-glyph.test.js +22 -0
  203. package/dist/test/model-resolution.test.js +166 -0
  204. package/dist/test/narrate-validate.test.js +77 -0
  205. package/dist/test/narrate-wait.test.js +160 -0
  206. package/dist/test/narrative-format.test.js +219 -0
  207. package/dist/test/narrative-menu.test.js +137 -0
  208. package/dist/test/narrative-sort.test.js +75 -0
  209. package/dist/test/no-pan.test.js +60 -0
  210. package/dist/test/pane-layout.test.js +153 -0
  211. package/dist/test/paths.test.js +37 -0
  212. package/dist/test/payload-inline.test.js +91 -0
  213. package/dist/test/precache-fonts.test.js +72 -0
  214. package/dist/test/presence-gate.test.js +440 -0
  215. package/dist/test/presence.test.js +160 -0
  216. package/dist/test/read-events.test.js +220 -0
  217. package/dist/test/remote-meld.test.js +115 -0
  218. package/dist/test/revive-cli.test.js +93 -0
  219. package/dist/test/search-floor.test.js +206 -0
  220. package/dist/test/search-fusion.test.js +168 -0
  221. package/dist/test/search.test.js +205 -0
  222. package/dist/test/security.test.js +48 -0
  223. package/dist/test/selection-wiring.test.js +61 -0
  224. package/dist/test/session-affinity.test.js +295 -0
  225. package/dist/test/signal-source.test.js +32 -0
  226. package/dist/test/socket-account.test.js +268 -0
  227. package/dist/test/spawn-toml.test.js +72 -0
  228. package/dist/test/speaker-color.test.js +90 -0
  229. package/dist/test/tailscale-rebind.test.js +79 -0
  230. package/dist/test/terminal-cursor.test.js +61 -0
  231. package/dist/test/terminal-resync.test.js +23 -0
  232. package/dist/test/thin-cli.test.js +382 -0
  233. package/dist/test/token-bootstrap.test.js +272 -0
  234. package/dist/test/token-devices.test.js +587 -0
  235. package/dist/test/token-lapse.test.js +94 -0
  236. package/dist/test/token-ring.test.js +188 -0
  237. package/dist/test/token-rotate-cli.test.js +58 -0
  238. package/dist/test/token-routes.test.js +95 -0
  239. package/dist/test/tool-uses.test.js +1006 -0
  240. package/dist/test/usage-ledger.test.js +126 -0
  241. package/dist/test/view-url.test.js +581 -0
  242. package/dist/test/wiki-links.test.js +57 -0
  243. package/dist/test/wiki-sources.test.js +85 -0
  244. package/dist/test/wiki-window.test.js +31 -0
  245. package/dist/test/wiki.test.js +399 -0
  246. package/dist/test/wordpiece.test.js +68 -0
  247. package/dist/tls.js +148 -0
  248. package/dist/tmux.js +202 -0
  249. package/dist/user-agent.js +46 -0
  250. package/docker-entrypoint.sh +141 -0
  251. package/docs/README.md +3 -0
  252. package/docs/absorb.md +231 -0
  253. package/docs/cost-ledger.md +125 -0
  254. package/docs/daemon-api.md +189 -0
  255. package/docs/diagrams.md +163 -0
  256. package/docs/doctor.md +36 -0
  257. package/docs/export-scope.md +69 -0
  258. package/docs/files-view.md +133 -0
  259. package/docs/link-previews.md +38 -0
  260. package/docs/math.md +32 -0
  261. package/docs/mcp.md +69 -0
  262. package/docs/memory-search.md +75 -0
  263. package/docs/mentions.md +101 -0
  264. package/docs/multi-brane.md +14 -0
  265. package/docs/passkeys.md +165 -0
  266. package/docs/presence.md +202 -0
  267. package/docs/releasing.md +53 -0
  268. package/docs/rendering-warnings.md +22 -0
  269. package/docs/request-layer.md +72 -0
  270. package/docs/selection-store.md +50 -0
  271. package/docs/session-affinity.md +61 -0
  272. package/docs/terminal-mirroring.md +211 -0
  273. package/docs/token-rotation.md +413 -0
  274. package/docs/view-urls.md +114 -0
  275. package/docs/web-app.md +23 -0
  276. package/docs/wiki.md +43 -0
  277. package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -0
  278. package/expo/dist/apple-touch-icon.png +0 -0
  279. package/expo/dist/favicon.svg +3 -0
  280. package/expo/dist/index.html +177 -0
  281. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  282. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  283. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  284. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  285. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  301. package/expo/dist/katex/katex.min.css +1 -0
  302. package/expo/dist/manifest.json +16 -0
  303. package/expo/dist/metadata.json +1 -0
  304. package/expo/dist/og/membrane.v1.png +0 -0
  305. package/expo/dist/pdf.min.mjs +28 -0
  306. package/expo/dist/pdf.worker.min.mjs +28 -0
  307. package/expo/dist/sw.js +154 -0
  308. package/package.json +81 -23
  309. package/protocols/LIVE_loop.md +407 -0
  310. package/protocols/bebrief.md +41 -0
  311. package/protocols/digest.md +231 -0
  312. package/protocols/fly.md +266 -0
  313. package/protocols/idle.md +81 -0
  314. package/protocols/keeper.md +829 -0
  315. package/protocols/markdown.md +80 -0
  316. package/protocols/memory.md +419 -0
  317. package/protocols/mentions.md +93 -0
  318. package/protocols/payloads.md +53 -0
  319. package/protocols/prelude.md +91 -0
  320. package/protocols/ramify.md +105 -0
  321. package/protocols/review.md +356 -0
  322. package/protocols/rrr.md +232 -0
  323. package/protocols/scribe.md +429 -0
  324. package/protocols/summarize.md +203 -0
  325. package/protocols/twin.md +174 -0
  326. package/protocols/wiki.md +438 -0
  327. package/protocols/worker.md +38 -0
  328. package/wiki/shared/append-only-event-log.md +148 -0
  329. package/wiki/shared/index.md +48 -0
  330. package/wiki/shared/owning-your-narratives.md +161 -0
  331. package/wiki/shared/premature-ontologies.md +435 -0
  332. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  333. package/wiki/shared/sequestering-the-narrative.md +143 -0
  334. package/wiki/shared/why-wikis-work.md +216 -0
  335. package/.npmignore +0 -2
  336. package/.travis.yml +0 -5
  337. package/index.js +0 -8
  338. package/index.js~ +0 -12
@@ -0,0 +1,123 @@
1
+ /** Inline-reference linkification over an mdast tree — the machinery behind
2
+ * both the narrative-id linkifier (`markdown-nids.ts`) and the protocol-command
3
+ * one (`markdown-protocols.ts`).
4
+ *
5
+ * A tree transform rather than a rewrite of the markdown source: code spans,
6
+ * fenced blocks, existing links and autolinked URLs are excluded
7
+ * *structurally* — the parser has already decided what they are, so this only
8
+ * ever sees prose. Source-level regexes can't make that distinction, and get
9
+ * it wrong on things like `https://brane.organelle.dev/claude-4c7289d1`.
10
+ */
11
+ /** Node types whose subtree is off-limits: their text is either already a
12
+ * link, a URL/label slot, or verbatim code. Nothing inside them is prose. */
13
+ const OPAQUE = new Set([
14
+ "link", "linkReference", "definition",
15
+ "image", "imageReference",
16
+ "code", "html",
17
+ ]);
18
+ // The renderer keys nodes off source position, and does so before extensions
19
+ // run — so nodes synthesized here have to bring their own keys. Derive them
20
+ // from the node being replaced (its own key when it has one, else its source
21
+ // position) so they stay stable across re-renders of the same content and
22
+ // don't depend on which other extensions are installed.
23
+ function keyOf(node) {
24
+ if (typeof node.key === "string" && node.key)
25
+ return node.key;
26
+ const start = node.position?.start;
27
+ return start ? `${node.type}-${start.line}:${start.column}` : node.type;
28
+ }
29
+ function makeLink(label, href, key) {
30
+ return {
31
+ type: "link",
32
+ url: href,
33
+ title: null,
34
+ key,
35
+ children: [{ type: "text", value: label, key: `${key}-label` }],
36
+ };
37
+ }
38
+ /** Splits a text node's value on references, returning the replacement node
39
+ * list — or null if it holds no linkable reference. */
40
+ function splitText(node, spec) {
41
+ const value = typeof node.value === "string" ? node.value : "";
42
+ const base = keyOf(node);
43
+ const re = new RegExp(spec.pattern, "g");
44
+ const out = [];
45
+ let last = 0;
46
+ let match;
47
+ while ((match = re.exec(value)) !== null) {
48
+ const start = match.index;
49
+ const end = start + match[0].length;
50
+ if (spec.boundary && !spec.boundary(value.slice(Math.max(0, start - 1), start), value.slice(end, end + 1))) {
51
+ continue;
52
+ }
53
+ const hit = spec.resolve(match);
54
+ if (hit === null)
55
+ continue;
56
+ const consumed = match[0].startsWith(hit.text) ? hit.text.length : match[0].length;
57
+ if (start > last) {
58
+ out.push({ type: "text", value: value.slice(last, start), key: `${base}-t${out.length}` });
59
+ }
60
+ out.push(makeLink(hit.text, hit.href, `${base}-ref${out.length}`));
61
+ last = start + consumed;
62
+ re.lastIndex = last;
63
+ }
64
+ if (out.length === 0)
65
+ return null;
66
+ if (last < value.length) {
67
+ out.push({ type: "text", value: value.slice(last), key: `${base}-t${out.length}` });
68
+ }
69
+ return out;
70
+ }
71
+ /** A code span that is *entirely* one reference becomes a link; anything else
72
+ * stays code. Agents habitually write `` `claude-4c7289d1:42` `` and
73
+ * `` `/ramify` ``. */
74
+ function codeSpanToLink(node, spec) {
75
+ const value = typeof node.value === "string" ? node.value : "";
76
+ const match = new RegExp(`^(?:${spec.pattern})$`).exec(value);
77
+ if (!match)
78
+ return null;
79
+ const hit = spec.resolve(match);
80
+ if (hit === null || hit.text !== value)
81
+ return null;
82
+ return makeLink(hit.text, hit.href, `${keyOf(node)}-ref`);
83
+ }
84
+ function walk(parent, spec) {
85
+ if (!Array.isArray(parent.children))
86
+ return;
87
+ const out = [];
88
+ let changed = false;
89
+ for (const child of parent.children) {
90
+ if (OPAQUE.has(child.type)) {
91
+ out.push(child);
92
+ }
93
+ else if (child.type === "inlineCode") {
94
+ const link = codeSpanToLink(child, spec);
95
+ if (link)
96
+ changed = true;
97
+ out.push(link ?? child);
98
+ }
99
+ else if (child.type === "text") {
100
+ const split = splitText(child, spec);
101
+ if (split)
102
+ changed = true;
103
+ out.push(...(split ?? [child]));
104
+ }
105
+ else {
106
+ walk(child, spec);
107
+ out.push(child);
108
+ }
109
+ }
110
+ if (changed)
111
+ parent.children = out;
112
+ }
113
+ /** Rewrites references into links, in place. */
114
+ export function linkifyRefs(tree, spec) {
115
+ walk(tree, spec);
116
+ }
117
+ /** `linkifyRefs` packaged as an mdast-util-from-markdown extension. Several can
118
+ * be installed at once: each runs after the last, and links a predecessor
119
+ * created are opaque to it. */
120
+ export function refLinks(spec) {
121
+ return { transforms: [(tree) => linkifyRefs(tree, spec)] };
122
+ }
123
+ //# sourceMappingURL=markdown-refs.js.map
@@ -0,0 +1,342 @@
1
+ /**
2
+ * New color on the 膜 film: splotches that a touch adds, and the invasion
3
+ * that changes the whole scheme every minute or so.
4
+ *
5
+ * **A tap adds a splotch, never a new film** (claude-1cwe8u9p:56). The spot
6
+ * turns at once, then its front advances slowly and against resistance
7
+ * (:49). Every splotch of a group shares one `pressure`; each tap adds to it
8
+ * and it drains over seconds. Above CRITICAL_PRESSURE fronts advance, below
9
+ * it the incumbent reclaims ground (more slowly than it was lost), and a
10
+ * wobble on each front makes it surge, stall and give way, so it reads as a
11
+ * struggle rather than a wipe.
12
+ *
13
+ * **One splotch dies out; two that touch prevail** (:58, :62). The game is
14
+ * "tap to preview a color; if you like it, tap a few more times to see it
15
+ * take over" (:86), and where the next tap lands decides what it means
16
+ * (claude-u6qlei0j:4, :7):
17
+ *
18
+ * - **On a splotch** (inside its outline as drawn): the caller recolors its
19
+ * whole group — same splotches, same size, another scheme. A tap on the
20
+ * spot you just tapped adds no area, so it means "not that color".
21
+ * - **Beside a splotch**, close enough that the new spot's outline would
22
+ * touch it: the tap joins its group. Same shades (:49), and now two that
23
+ * touch, so the color is committed. At any pace.
24
+ * - Otherwise, taps in a quick burst (within BURST_S of each other) form
25
+ * one group, and a tap on bare film after that is a new color to preview.
26
+ *
27
+ * A splotch whose outline reaches another of its group — or of a group
28
+ * whose scheme the caller judges similar — is anchored and keeps the ground
29
+ * it has held. An isolated splotch heals and is gone.
30
+ *
31
+ * **A wash is closed** (claude-u6qlei0j:3). Its splotches cover most of the
32
+ * glyph while it runs, so a tap during one used to land "on" it and join
33
+ * it: a splotch in the wash's own color, invisible, and pressure a wash
34
+ * ignores. Now a washing group is neither recolored nor joined; a tap on it
35
+ * starts a new group, which paints over the wash (born later) and outlives
36
+ * its adoption, so the glyph answers a tap at every moment.
37
+ *
38
+ * **What prevails washes back into the flow, in its colors** (:73, :74,
39
+ * :77). A stain that stayed put left a patchwork of frozen borders. So
40
+ * WASH_DELAY_S after a group anchors it starts to wash: its pressure is held
41
+ * up, its splotches lose their size cap, and they rise — an outline reaches
42
+ * up to RISE times farther upward than down or sideways, so the new colors
43
+ * climb with the film's lift, in the stain's own shape. When the group
44
+ * covers the glyph its scheme becomes the incumbent and its splotches go.
45
+ * The film is the default flow again, in the colors that were painted.
46
+ *
47
+ * **Splotches are not circles** (:61). Each has its own outline: stretched
48
+ * along a random axis (area-preserving), with 2-, 3- and 5-lobed bulges of
49
+ * random size and phase. `sdAt` and `reach` here are the same outline the
50
+ * shader draws, less its fine noise dithering, so what counts as touching
51
+ * is what looks like touching.
52
+ *
53
+ * **No limits a hand will meet** (:61). Every splotch either heals or
54
+ * washes into the film within seconds, so few are ever alive at once;
55
+ * MAX_SPLOTCHES bounds that, and at it a tap still lands — it evicts the
56
+ * smallest splotch not anchored, or failing that the smallest of all.
57
+ *
58
+ * **The automatic change** is the same wash, from one round splotch planted
59
+ * somewhere in the glyph.
60
+ *
61
+ * The TUNING block is where "how easily does a splotch live" is decided
62
+ * (:63). Positions are glyph-normalized (the glyph spans -0.5..0.5, y down),
63
+ * the space the shader's `p` lives in. Window-free and deterministic given
64
+ * its random source, so `src/test/membrane-invasion.test.ts` can pin it.
65
+ */
66
+ // ---- TUNING ----
67
+ /** The spot a tap turns at once. */
68
+ export const TAP_RADIUS = 0.09;
69
+ /** Largest a tap's splotch grows (before stretch and lobes). */
70
+ export const MAX_SPLOTCH_RADIUS = 0.22;
71
+ /** Taps this close together in time are one burst: one group, one scheme. */
72
+ export const BURST_S = 2.5;
73
+ /** A tap this far *inside* a splotch's outline is on it (recolors it); nearer
74
+ * the edge than this, it is beside it (joins it), since a finger meaning
75
+ * "more of this" lands on the edge as often as just outside it. */
76
+ export const TAP_EDGE = 0.02;
77
+ export const PRESSURE_PER_TAP = 1;
78
+ export const PRESSURE_DRAIN_S = 30;
79
+ /** Pressure at which a front neither advances nor retreats. */
80
+ export const CRITICAL_PRESSURE = 0.8;
81
+ /** Pressure above critical at which a front reaches MAX_SPEED. */
82
+ export const PRESSURE_SPAN = 1.5;
83
+ /** Fastest a front ever moves, glyph widths per second. */
84
+ export const MAX_SPEED = 0.032;
85
+ /** The incumbent reclaims at this fraction of the speed it loses ground. */
86
+ export const RECLAIM = 0.35;
87
+ /** Per-front surge and stall, glyph widths per second. */
88
+ export const WOBBLE = 0.015;
89
+ /** How irregular a splotch is: stretch range, and the largest bulge per lobe count. */
90
+ export const ASPECT = [1.2, 2.4];
91
+ export const LOBE_MAX = [0.25, 0.2, 0.12];
92
+ /** How long a group that prevails struggles before it washes into the film. */
93
+ export const WASH_DELAY_S = 2;
94
+ /** How fast a wash's fronts advance, glyph widths per second. */
95
+ export const WASH_SPEED = 0.05;
96
+ /** How much farther upward than down a washing outline reaches, and how
97
+ * long it takes to get there. */
98
+ export const RISE = 3;
99
+ export const RISE_IN_S = 2;
100
+ // ---- end TUNING ----
101
+ /**
102
+ * How far inside a wash's outline every glyph point must be before the wash
103
+ * is adopted as the film. It has to exceed everything that can make the
104
+ * rendered front differ from `sdAt`: the shader's noise dithering (0.16 of
105
+ * a fbm offset of at most ±0.4, so ±0.064), the half-width of its edge
106
+ * (0.012), and the gap between sample points. Adopting a wash that covers
107
+ * the glyph by this much changes no pixel (claude-1cwe8u9p:81).
108
+ */
109
+ export const COVER_MARGIN = 0.1;
110
+ /** Room in the shader's splotch table. */
111
+ export const MAX_SPLOTCHES = 64;
112
+ /** Texels per splotch row in the shader's table. */
113
+ export const SPLOTCH_TEXELS = 4;
114
+ export const emptyFilm = () => ({ splotches: [], groups: [], clock: 0, nextId: 1 });
115
+ const ROUND = { angle: 0, aspect: 1, lobes: [0, 0, 0], phases: [0, 0, 0] };
116
+ /** A random irregular outline. */
117
+ export function randomShape(rng) {
118
+ const range = (lo, hi) => lo + (hi - lo) * rng();
119
+ return {
120
+ angle: rng() * Math.PI,
121
+ aspect: range(ASPECT[0], ASPECT[1]),
122
+ lobes: [rng() * LOBE_MAX[0], rng() * LOBE_MAX[1], rng() * LOBE_MAX[2]],
123
+ phases: [rng() * Math.PI * 2, rng() * Math.PI * 2, rng() * Math.PI * 2],
124
+ };
125
+ }
126
+ /** The outline's bulge at local angle `th`: 1 for a circle. */
127
+ function bulge(shape, th) {
128
+ const [a2, a3, a5] = shape.lobes, [p2, p3, p5] = shape.phases;
129
+ return 1 + a2 * Math.cos(2 * th + p2) + a3 * Math.cos(3 * th + p3) + a5 * Math.cos(5 * th + p5);
130
+ }
131
+ /** Into the splotch's own frame: upward distances shrunk by its rise (y is
132
+ * down, so up is negative), then rotated to its axis. */
133
+ function local(s, x, y) {
134
+ const dx = x - s.x, dy0 = y - s.y, dy = dy0 < 0 ? dy0 / s.rise : dy0;
135
+ const c = Math.cos(s.shape.angle), n = Math.sin(s.shape.angle);
136
+ return [c * dx + n * dy, -n * dx + c * dy];
137
+ }
138
+ /** Signed distance-like measure from (x, y) to the splotch's outline:
139
+ * negative inside. The shader's, less its noise dithering. */
140
+ export function sdAt(s, x, y) {
141
+ const [lx, ly] = local(s, x, y), k = Math.sqrt(s.shape.aspect);
142
+ return Math.hypot(lx / k, ly * k) - s.r * bulge(s.shape, Math.atan2(ly, lx));
143
+ }
144
+ /** How far the outline reaches from the centre toward (x, y). */
145
+ export function reach(s, x, y) {
146
+ const d = Math.hypot(x - s.x, y - s.y);
147
+ if (d === 0)
148
+ return 0;
149
+ // Along a ray from the centre the frame change is linear, so the measure
150
+ // grows in proportion to distance: find it for a unit step, and scale.
151
+ const ux = s.x + (x - s.x) / d, uy = s.y + (y - s.y) / d;
152
+ const [lx, ly] = local(s, ux, uy), k = Math.sqrt(s.shape.aspect);
153
+ const perUnit = Math.hypot(lx / k, ly * k);
154
+ return Math.max(0, (s.r * bulge(s.shape, Math.atan2(ly, lx))) / perUnit);
155
+ }
156
+ /** Whether two outlines touch, along the line between their centres. */
157
+ export function touching(a, b) {
158
+ return a.r > 0 && b.r > 0 && Math.hypot(a.x - b.x, a.y - b.y) < reach(a, b.x, b.y) + reach(b, a.x, a.y);
159
+ }
160
+ /** 1D gradient noise in [-0.5, 0.5], smooth in x. */
161
+ function noise1(x) {
162
+ const i = Math.floor(x), f = x - i;
163
+ const g = (n) => {
164
+ const h = Math.sin(n * 127.1 + 311.7) * 43758.5453;
165
+ return 2 * (h - Math.floor(h)) - 1;
166
+ };
167
+ const u = f * f * f * (f * (f * 6 - 15) + 10);
168
+ return (g(i) * f) * (1 - u) + (g(i + 1) * (f - 1)) * u;
169
+ }
170
+ /** Make room for one more splotch: drop the smallest that isn't anchored, or
171
+ * failing that the smallest of all. */
172
+ function makeRoom(splotches) {
173
+ if (splotches.length < MAX_SPLOTCHES)
174
+ return splotches;
175
+ const pool = splotches.some((s) => s.anchoredAt === null) ? splotches.filter((s) => s.anchoredAt === null) : splotches;
176
+ const victim = pool.reduce((a, s) => (s.r < a.r ? s : a));
177
+ return splotches.filter((s) => s !== victim);
178
+ }
179
+ /**
180
+ * A touch at (x, y). On a splotch, it recolors that splotch's group; beside
181
+ * one, it joins it; otherwise it starts a group, or joins the latest burst.
182
+ * The topmost splotch under the finger decides, and a washing group counts
183
+ * as film, not as a splotch (see the header).
184
+ */
185
+ export function tap(film, x, y, rng) {
186
+ const byId = new Map(film.groups.map((g) => [g.id, g]));
187
+ const open = (s) => !washing(byId.get(s.group), film.clock);
188
+ const live = layered(film).filter(open);
189
+ const on = live.filter((s) => sdAt(s, x, y) <= -TAP_EDGE).at(-1);
190
+ if (on) {
191
+ const groups = film.groups.map((g) => (g.id === on.group ? { ...g, lastTap: film.clock } : g));
192
+ return { film: { ...film, groups }, started: null, recolored: groups.find((g) => g.id === on.group) };
193
+ }
194
+ // Beside: the spot this tap would turn reaches an outline (its edge counts
195
+ // as beside, see TAP_EDGE). Otherwise, quick taps anywhere are one burst.
196
+ const spot = { x, y, r: TAP_RADIUS, phase: 0, group: 0, shape: ROUND, anchoredAt: null, rise: 1 };
197
+ const beside = live.filter((s) => touching(spot, s)).at(-1);
198
+ const onto = beside ? byId.get(beside.group) : null;
199
+ const latest = film.groups
200
+ .filter((g) => !g.auto && !washing(g, film.clock))
201
+ .reduce((a, g) => (!a || g.lastTap > a.lastTap ? g : a), null);
202
+ const joins = onto ?? (latest && film.clock - latest.lastTap <= BURST_S ? latest : null);
203
+ const started = joins ? null : { id: film.nextId, pressure: 0, born: film.clock, lastTap: film.clock, auto: false, washAt: null };
204
+ const id = (joins ?? started).id;
205
+ const groups = (started ? [...film.groups, started] : film.groups)
206
+ .map((g) => (g.id === id ? { ...g, pressure: g.pressure + PRESSURE_PER_TAP, lastTap: film.clock } : g));
207
+ const splotch = { ...spot, phase: rng() * 1000, group: id, shape: randomShape(rng) };
208
+ const splotches = [...makeRoom(film.splotches), splotch];
209
+ const alive = new Set(splotches.map((s) => s.group));
210
+ return {
211
+ film: { ...film, groups: groups.filter((g) => alive.has(g.id)), splotches, nextId: started ? film.nextId + 1 : film.nextId },
212
+ started: started ? groups.find((g) => g.id === id) : null,
213
+ recolored: null,
214
+ };
215
+ }
216
+ /** The film dragged by (dx, dy) glyph widths: every splotch comes along, so
217
+ * what has been painted stays on the film it was painted on rather than
218
+ * parked on the glyph while the film moves under it (claude-u6qlei0j:23). */
219
+ export function shift(film, dx, dy) {
220
+ if (dx === 0 && dy === 0)
221
+ return film;
222
+ return { ...film, splotches: film.splotches.map((s) => ({ ...s, x: s.x + dx, y: s.y + dy })) };
223
+ }
224
+ /** Plant the automatic change at (x, y). Null when one is already under way. */
225
+ export function invade(film, x, y, rng) {
226
+ if (film.groups.some((g) => g.auto))
227
+ return null;
228
+ const started = { id: film.nextId, pressure: 0, born: film.clock, lastTap: film.clock, auto: true, washAt: film.clock };
229
+ // No tap to explain it, so it grows from nothing rather than appearing.
230
+ const splotch = { x, y, r: 0.005, phase: rng() * 1000, group: started.id, shape: ROUND, anchoredAt: null, rise: 1 };
231
+ const splotches = [...makeRoom(film.splotches), splotch];
232
+ const alive = new Set(splotches.map((s) => s.group));
233
+ return {
234
+ film: { ...film, groups: [...film.groups, started].filter((g) => alive.has(g.id)), splotches, nextId: film.nextId + 1 },
235
+ started,
236
+ };
237
+ }
238
+ /**
239
+ * Whether the automatic change is due: `wall` seconds since load have passed
240
+ * `holdUntil`, and none is under way. `wall` is **wall time**, not the film's
241
+ * clock, on purpose. The frame loop stops while the tab is hidden or the
242
+ * glyph is off screen, so the film's clock stands still through an absence
243
+ * while the hold runs on — and a return after longer than the hold plants
244
+ * the invasion on the very first frame back, a drama the operator asked to
245
+ * keep (claude-xaj5pb9b:18). Measure the hold on the film's clock instead
246
+ * and that moment goes away. `src/test/membrane-invasion.test.ts` pins it.
247
+ */
248
+ export function autoDue(film, holdUntil, wall) {
249
+ return wall >= holdUntil && !film.groups.some((g) => g.auto);
250
+ }
251
+ /** Whether a group is washing into the film at `clock`. */
252
+ export function washing(g, clock) {
253
+ return g.washAt !== null && clock >= g.washAt;
254
+ }
255
+ /** How fast every front of a group moves at this pressure, before its wobble. */
256
+ export function frontSpeed(pressure) {
257
+ const k = Math.max(-1, Math.min(1, (pressure - CRITICAL_PRESSURE) / PRESSURE_SPAN));
258
+ return MAX_SPEED * (k >= 0 ? k : k * RECLAIM);
259
+ }
260
+ export function step(film, dt, similar = () => false) {
261
+ const h = Math.max(0, Math.min(dt, 0.1));
262
+ const clock = film.clock + h;
263
+ const byIdBefore = new Map(film.groups.map((g) => [g.id, g]));
264
+ const kin = (a, b) => {
265
+ const ga = byIdBefore.get(a.group), gb = byIdBefore.get(b.group);
266
+ if (ga.auto || gb.auto)
267
+ return false;
268
+ return ga.id === gb.id || similar(ga, gb);
269
+ };
270
+ const anchoredNow = new Set(film.splotches.filter((s) => s.anchoredAt !== null || film.splotches.some((o) => o !== s && kin(s, o) && touching(s, o))));
271
+ const groups = film.groups.map((g) => {
272
+ // A group prevails when any of its splotches anchors; it washes shortly after.
273
+ const prevailed = g.washAt === null && film.splotches.some((s) => s.group === g.id && anchoredNow.has(s));
274
+ return {
275
+ ...g,
276
+ washAt: prevailed ? clock + WASH_DELAY_S : g.washAt,
277
+ pressure: g.pressure * Math.exp(-h / PRESSURE_DRAIN_S),
278
+ };
279
+ });
280
+ const byId = new Map(groups.map((g) => [g.id, g]));
281
+ const splotches = film.splotches
282
+ .map((s) => {
283
+ const g = byId.get(s.group);
284
+ const washes = washing(g, clock);
285
+ const anchored = anchoredNow.has(s);
286
+ const v = washes ? WASH_SPEED : frontSpeed(g.pressure);
287
+ let r = s.r + (v + WOBBLE * 2 * noise1(clock * 0.5 + s.phase)) * h;
288
+ if (!washes)
289
+ r = Math.min(MAX_SPLOTCH_RADIUS, r);
290
+ // Anchored ground is kept: the front may still advance, never retreat.
291
+ if (anchored || washes)
292
+ r = Math.max(s.r, r);
293
+ const rise = washes ? Math.min(RISE, s.rise + (h * (RISE - 1)) / RISE_IN_S) : s.rise;
294
+ return { ...s, r: Math.max(0, r), anchoredAt: anchored ? r : null, rise };
295
+ })
296
+ .filter((s) => s.r > 0);
297
+ const alive = new Set(splotches.map((s) => s.group));
298
+ return { ...film, clock, groups: groups.filter((g) => alive.has(g.id)), splotches };
299
+ }
300
+ /** Whether a group's splotches hold every one of these points, by `margin`. */
301
+ export function covers(film, groupId, points, margin = COVER_MARGIN) {
302
+ const mine = film.splotches.filter((s) => s.group === groupId);
303
+ return mine.length > 0 && points.every(([x, y]) => mine.some((s) => sdAt(s, x, y) <= -margin));
304
+ }
305
+ /** Groups composite oldest first, each group's splotches together, so a
306
+ * newer group always paints over an older one. */
307
+ function layered(film) {
308
+ const born = new Map(film.groups.map((g) => [g.id, g.born]));
309
+ return film.splotches
310
+ .map((s, i) => ({ s, i, b: born.get(s.group) ?? 0 }))
311
+ .sort((a, b) => a.b - b.b || a.i - b.i)
312
+ .map(({ s }) => s);
313
+ }
314
+ /**
315
+ * The film after a group has won: its scheme is now the incumbent, so its
316
+ * splotches go — and so does every group born before it, which composited
317
+ * beneath it and so is already hidden everywhere it covers, which is
318
+ * everywhere. Dropping them changes nothing visible; keeping them would let
319
+ * them resurface over the new incumbent (claude-1cwe8u9p:81).
320
+ */
321
+ export function adopt(film, groupId) {
322
+ const winner = film.groups.find((g) => g.id === groupId);
323
+ if (!winner)
324
+ return film;
325
+ const stays = new Set(film.groups.filter((g) => g.id !== groupId && g.born > winner.born).map((g) => g.id));
326
+ return { ...film, splotches: film.splotches.filter((s) => stays.has(s.group)), groups: film.groups.filter((g) => stays.has(g.id)) };
327
+ }
328
+ /**
329
+ * The shader's splotch table, oldest group first so newer paint over older: per
330
+ * splotch SPLOTCH_TEXELS RGBA texels —
331
+ * (x, y, r, scheme row), (angle, aspect, a2, a3), (a5, p2, p3, p5), (rise, 0, 0, 0)
332
+ * — with `row(group)` naming where the caller keeps that group's scheme.
333
+ */
334
+ export function packSplotches(film, row) {
335
+ const out = new Float32Array(MAX_SPLOTCHES * SPLOTCH_TEXELS * 4);
336
+ layered(film).slice(0, MAX_SPLOTCHES).forEach((s, i) => {
337
+ const { angle, aspect, lobes: [a2, a3, a5], phases: [p2, p3, p5] } = s.shape;
338
+ out.set([s.x, s.y, s.r, row(s.group), angle, aspect, a2, a3, a5, p2, p3, p5, s.rise, 0, 0, 0], i * SPLOTCH_TEXELS * 4);
339
+ });
340
+ return out;
341
+ }
342
+ //# sourceMappingURL=membrane-invasion.js.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * How the 膜 film moves, as numbers the component can be handed.
3
+ *
4
+ * The film in `expo/components/Membrane.web.tsx` is Perlin noise read through
5
+ * a moving window: `lift` carries the window up (so structure forms below and
6
+ * dissolves above), `sway` carries it sideways, and `churn` is how fast the
7
+ * noise evolves in place. The window's position and the noise's time phase
8
+ * are *integrated* here, frame by frame, rather than computed as `rate * t`
9
+ * in the shader: with `t` in the hundreds of seconds, changing a rate under
10
+ * `rate * t` teleports the whole field, and the motion is meant to change
11
+ * while you watch it. An awakening should read as the film starting to rise.
12
+ *
13
+ * A hand on the film holds it: while `held`, the drift stops at once (a
14
+ * grip, not an ease) and `dragMotion` moves the window with the finger, so
15
+ * the film follows it 1:1 (claude-u6qlei0j:6). Letting go eases the drift
16
+ * back in. The churn keeps turning under a hand, so a held film still lives.
17
+ *
18
+ * `voice` names the motion by what it means. The glyph is the brane's
19
+ * touch-and-hold-to-talk button in waiting (claude-1ejyovuv:115, :120), and
20
+ * how it moves says whether the brane's voice is live or would need waking.
21
+ * Window-free, so `src/test/membrane-motion.test.ts` can pin it.
22
+ */
23
+ export const MEMBRANE_MOTION = {
24
+ // Half the lift #465 shipped with (0.14), which read as busier than a
25
+ // resting surface should.
26
+ awake: { lift: 0.07, sway: 0.012, churn: 1 },
27
+ // Settled but alive: nothing rises, the swirl keeps turning slowly.
28
+ dormant: { lift: 0, sway: 0.012, churn: 0.4 },
29
+ };
30
+ export const DEFAULT_MEMBRANE_VOICE = "awake";
31
+ /** The motion for a voice state, with any explicit rate winning over the preset. */
32
+ export function membraneMotion(voice = DEFAULT_MEMBRANE_VOICE, overrides = {}) {
33
+ const base = MEMBRANE_MOTION[voice];
34
+ return {
35
+ lift: overrides.lift ?? base.lift,
36
+ sway: overrides.sway ?? base.sway,
37
+ churn: overrides.churn ?? base.churn,
38
+ };
39
+ }
40
+ /** Time constant of the ease toward a new motion: ~95% there after 3τ ≈ 1 s. */
41
+ export const MOTION_EASE_S = 0.35;
42
+ /** Longest step integrated at once. The loop stops while the glyph is off
43
+ * screen or the tab hidden, and the first frame back must continue from where
44
+ * the film was left rather than jump ahead by the whole absence. */
45
+ export const MOTION_MAX_DT_S = 0.1;
46
+ export function initialMotionState(motion) {
47
+ return { motion: { ...motion }, offset: [0, 0], phase: 0 };
48
+ }
49
+ /** The shader reads the noise at `p * NOISE_SCALE + offset`, glyph-normalized
50
+ * `p`; a drag of `d` glyph widths is an offset change of `NOISE_SCALE * d`. */
51
+ export const NOISE_SCALE = 2.4;
52
+ /**
53
+ * Advance by `dt` seconds toward `target`: ease the rates, then integrate the
54
+ * position and phase at the eased rates. Position therefore only ever moves
55
+ * by rate × dt, whatever the target does, so no change of motion can make the
56
+ * field jump. While `held`, the drift is zero at once rather than eased: a
57
+ * hand stops a film, and it is the release that eases back in.
58
+ */
59
+ export function stepMotion(state, target, dt, held = false) {
60
+ const h = Math.min(Math.max(dt, 0), MOTION_MAX_DT_S);
61
+ const k = 1 - Math.exp(-h / MOTION_EASE_S);
62
+ const ease = (from, to) => from + (to - from) * k;
63
+ const motion = {
64
+ lift: held ? 0 : ease(state.motion.lift, target.lift),
65
+ sway: held ? 0 : ease(state.motion.sway, target.sway),
66
+ churn: ease(state.motion.churn, target.churn),
67
+ };
68
+ return {
69
+ motion,
70
+ offset: [state.offset[0] + motion.sway * h, state.offset[1] + motion.lift * h],
71
+ phase: state.phase + motion.churn * h,
72
+ };
73
+ }
74
+ /**
75
+ * The film dragged by (dx, dy) glyph widths (y down, as the shader's `p`):
76
+ * the noise window moves the other way by NOISE_SCALE times as much, so the
77
+ * pattern under the finger stays under it. The one way the field moves by
78
+ * more than rate × dt, and it is the hand's own motion.
79
+ */
80
+ export function dragMotion(state, dx, dy) {
81
+ return { ...state, offset: [state.offset[0] - NOISE_SCALE * dx, state.offset[1] - NOISE_SCALE * dy] };
82
+ }
83
+ //# sourceMappingURL=membrane-motion.js.map