brane 0.1.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +367 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +101 -0
- package/DESIGN.md +405 -0
- package/Dockerfile +61 -0
- package/LICENSE +656 -17
- package/README.md +174 -2
- package/SECURITY.md +213 -0
- package/dist/absorb/brane-id.js +11 -0
- package/dist/absorb/chatgpt.js +161 -0
- package/dist/absorb/claude-code.js +504 -0
- package/dist/absorb/index.js +543 -0
- package/dist/absorb/organelle.js +345 -0
- package/dist/absorb/run.js +520 -0
- package/dist/absorb/sources.js +168 -0
- package/dist/absorb/types.js +3 -0
- package/dist/agent-env.js +45 -0
- package/dist/agents.js +1168 -0
- package/dist/claude-live-session.js +124 -0
- package/dist/cli.js +50 -0
- package/dist/codex-tool-log.js +144 -0
- package/dist/commands/absorb.js +291 -0
- package/dist/commands/attach.js +31 -0
- package/dist/commands/certify.js +62 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/cost.js +126 -0
- package/dist/commands/daemon.js +350 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/fold.js +95 -0
- package/dist/commands/list.js +89 -0
- package/dist/commands/mcp.js +84 -0
- package/dist/commands/meld.js +360 -0
- package/dist/commands/memory.js +215 -0
- package/dist/commands/revive.js +74 -0
- package/dist/commands/run.js +85 -0
- package/dist/commands/spawn.js +1101 -0
- package/dist/commands/token.js +154 -0
- package/dist/copilot-tool-log.js +145 -0
- package/dist/cost/pricing.js +116 -0
- package/dist/cost/reprice.js +136 -0
- package/dist/crush-tool-log.js +293 -0
- package/dist/daemon/agent-scope.js +116 -0
- package/dist/daemon/awaiting.js +54 -0
- package/dist/daemon/cert-manager.js +349 -0
- package/dist/daemon/client.js +262 -0
- package/dist/daemon/gated-routes.js +112 -0
- package/dist/daemon/keep-awake.js +103 -0
- package/dist/daemon/local-socket.js +145 -0
- package/dist/daemon/presence-gate.js +206 -0
- package/dist/daemon/server.js +6353 -0
- package/dist/daemon/signal-source.js +35 -0
- package/dist/daemon/tailscale-rebind.js +58 -0
- package/dist/daemon/token-ring.js +263 -0
- package/dist/daemon/token-routes.js +190 -0
- package/dist/db/events.js +152 -0
- package/dist/db/mentions.js +179 -0
- package/dist/db/schema.js +643 -0
- package/dist/db/tokens.js +624 -0
- package/dist/db/tool-uses.js +647 -0
- package/dist/diagram/index.js +85 -0
- package/dist/diagram/label.js +455 -0
- package/dist/diagram/layout.js +476 -0
- package/dist/diagram/links.js +50 -0
- package/dist/diagram/metrics.js +152 -0
- package/dist/diagram/parse.js +487 -0
- package/dist/diagram/svg.js +389 -0
- package/dist/diagram/types.js +9 -0
- package/dist/export.js +629 -0
- package/dist/files/archive-types.js +2 -0
- package/dist/files/archive.js +182 -0
- package/dist/files/browse.js +327 -0
- package/dist/files/bytes.js +162 -0
- package/dist/files/codecs.js +33 -0
- package/dist/files/find.js +212 -0
- package/dist/files/tar.js +223 -0
- package/dist/files/zip.js +209 -0
- package/dist/folds.js +72 -0
- package/dist/grammars/ebnf.js +92 -0
- package/dist/highlight.js +250 -0
- package/dist/ids.js +24 -0
- package/dist/markdown-diagnostics.js +233 -0
- package/dist/math.js +168 -0
- package/dist/mcp/server.js +886 -0
- package/dist/memory/embedder.js +145 -0
- package/dist/memory/embedding-worker.js +71 -0
- package/dist/memory/embeddings.js +91 -0
- package/dist/memory/index.js +232 -0
- package/dist/memory/storage.js +587 -0
- package/dist/memory/wiki.js +688 -0
- package/dist/memory/wordpiece.js +247 -0
- package/dist/mentions-scan.js +40 -0
- package/dist/presence.js +259 -0
- package/dist/session-affinity.js +185 -0
- package/dist/shared/api-request.js +124 -0
- package/dist/shared/app-origin.js +26 -0
- package/dist/shared/block-glyphs.js +114 -0
- package/dist/shared/diagram-css.js +17 -0
- package/dist/shared/export-selection.js +101 -0
- package/dist/shared/find-in-text.js +110 -0
- package/dist/shared/glow-hues.js +225 -0
- package/dist/shared/hex-dump.js +62 -0
- package/dist/shared/hex-pages.js +52 -0
- package/dist/shared/hit-drum.js +79 -0
- package/dist/shared/known-nids.js +58 -0
- package/dist/shared/list-scroll-keys.js +56 -0
- package/dist/shared/markdown-lists.js +72 -0
- package/dist/shared/markdown-math.js +485 -0
- package/dist/shared/markdown-mentions.js +79 -0
- package/dist/shared/markdown-nids.js +41 -0
- package/dist/shared/markdown-protocols.js +53 -0
- package/dist/shared/markdown-refs.js +123 -0
- package/dist/shared/membrane-invasion.js +342 -0
- package/dist/shared/membrane-motion.js +83 -0
- package/dist/shared/membrane-palette.js +253 -0
- package/dist/shared/membrane-terrain.js +212 -0
- package/dist/shared/message-selection.js +123 -0
- package/dist/shared/mo-glyph.js +57 -0
- package/dist/shared/narrative-format.js +237 -0
- package/dist/shared/narrative-menu.js +84 -0
- package/dist/shared/narrative-sort.js +47 -0
- package/dist/shared/narrative-status.js +52 -0
- package/dist/shared/no-pan.js +85 -0
- package/dist/shared/pane-layout.js +50 -0
- package/dist/shared/payload-inline.js +83 -0
- package/dist/shared/speaker-color.js +61 -0
- package/dist/shared/token-rotation.js +14 -0
- package/dist/shared/view-url.js +216 -0
- package/dist/shared/wiki-links.js +70 -0
- package/dist/shared/wiki-window.js +4 -0
- package/dist/tailscale.js +55 -0
- package/dist/test/absorb-claude.test.js +892 -0
- package/dist/test/agent-scope.test.js +193 -0
- package/dist/test/agents.test.js +55 -0
- package/dist/test/api-request.test.js +122 -0
- package/dist/test/app-host-literal.test.js +53 -0
- package/dist/test/awaiting-glow.test.js +54 -0
- package/dist/test/awaiting.test.js +122 -0
- package/dist/test/bind-token.test.js +88 -0
- package/dist/test/block-glyphs.test.js +68 -0
- package/dist/test/cert-manager.test.js +262 -0
- package/dist/test/claude-live-session.test.js +110 -0
- package/dist/test/codex-tool-log.test.js +568 -0
- package/dist/test/config.test.js +71 -0
- package/dist/test/copilot-tool-log.test.js +93 -0
- package/dist/test/cors-preflight.test.js +56 -0
- package/dist/test/cost-linger.test.js +31 -0
- package/dist/test/cost-pricing.test.js +72 -0
- package/dist/test/cost-reprice.test.js +139 -0
- package/dist/test/cost-sessions.test.js +301 -0
- package/dist/test/crush-tool-log.test.js +289 -0
- package/dist/test/daemon-client.test.js +109 -0
- package/dist/test/daemon-socket.test.js +293 -0
- package/dist/test/diagram-arrows.test.js +205 -0
- package/dist/test/diagram-export.test.js +210 -0
- package/dist/test/diagram-labels.test.js +313 -0
- package/dist/test/diagram-parse.test.js +191 -0
- package/dist/test/diagram-render.test.js +175 -0
- package/dist/test/diagram-review.test.js +288 -0
- package/dist/test/diagram-self-loop-labels.test.js +303 -0
- package/dist/test/embedder.test.js +84 -0
- package/dist/test/events.test.js +123 -0
- package/dist/test/export-selection.test.js +102 -0
- package/dist/test/export.test.js +505 -0
- package/dist/test/files-browse.test.js +393 -0
- package/dist/test/files-find.test.js +272 -0
- package/dist/test/find-in-text.test.js +86 -0
- package/dist/test/first-paint-viewport.test.js +59 -0
- package/dist/test/fixtures/embedding-golden.js +23 -0
- package/dist/test/fixtures/wordpiece-golden.js +167 -0
- package/dist/test/glow-hues.test.js +202 -0
- package/dist/test/harness-cost.test.js +423 -0
- package/dist/test/helpers.js +21 -0
- package/dist/test/hex-pages.test.js +68 -0
- package/dist/test/highlight.test.js +69 -0
- package/dist/test/hit-drum.test.js +71 -0
- package/dist/test/hook-config.test.js +299 -0
- package/dist/test/json-gzip.test.js +70 -0
- package/dist/test/key-routing.test.js +58 -0
- package/dist/test/known-nids.test.js +106 -0
- package/dist/test/link-preview.test.js +67 -0
- package/dist/test/list-scroll-keys.test.js +66 -0
- package/dist/test/listen-own-words.test.js +147 -0
- package/dist/test/listen-timeout.test.js +33 -0
- package/dist/test/markdown-diagnostics.test.js +121 -0
- package/dist/test/markdown-lists.test.js +182 -0
- package/dist/test/markdown-math.test.js +256 -0
- package/dist/test/markdown-nids.test.js +146 -0
- package/dist/test/markdown-protocols.test.js +139 -0
- package/dist/test/mcp-bridge-cancel.test.js +98 -0
- package/dist/test/mcp-bridge-reconnect.test.js +121 -0
- package/dist/test/mcp-narrative.test.js +193 -0
- package/dist/test/mcp.test.js +548 -0
- package/dist/test/meld.test.js +143 -0
- package/dist/test/membrane-invasion.test.js +482 -0
- package/dist/test/membrane-motion.test.js +101 -0
- package/dist/test/membrane-palette.test.js +166 -0
- package/dist/test/membrane-terrain.test.js +170 -0
- package/dist/test/memory-log.test.js +202 -0
- package/dist/test/mentions-e2e.test.js +261 -0
- package/dist/test/mentions-ledger.test.js +265 -0
- package/dist/test/mentions-listen.test.js +265 -0
- package/dist/test/mentions.test.js +238 -0
- package/dist/test/message-selection.test.js +114 -0
- package/dist/test/mo-glyph.test.js +22 -0
- package/dist/test/model-resolution.test.js +166 -0
- package/dist/test/narrate-validate.test.js +77 -0
- package/dist/test/narrate-wait.test.js +160 -0
- package/dist/test/narrative-format.test.js +219 -0
- package/dist/test/narrative-menu.test.js +150 -0
- package/dist/test/narrative-sort.test.js +75 -0
- package/dist/test/no-pan.test.js +60 -0
- package/dist/test/pane-layout.test.js +153 -0
- package/dist/test/paths.test.js +37 -0
- package/dist/test/payload-inline.test.js +91 -0
- package/dist/test/precache-fonts.test.js +72 -0
- package/dist/test/presence-gate.test.js +440 -0
- package/dist/test/presence.test.js +160 -0
- package/dist/test/read-events.test.js +220 -0
- package/dist/test/remote-meld.test.js +128 -0
- package/dist/test/revive-cli.test.js +93 -0
- package/dist/test/search-floor.test.js +206 -0
- package/dist/test/search-fusion.test.js +168 -0
- package/dist/test/search.test.js +205 -0
- package/dist/test/security.test.js +48 -0
- package/dist/test/selection-wiring.test.js +61 -0
- package/dist/test/session-affinity.test.js +295 -0
- package/dist/test/signal-source.test.js +32 -0
- package/dist/test/socket-account.test.js +268 -0
- package/dist/test/spawn-toml.test.js +72 -0
- package/dist/test/speaker-color.test.js +90 -0
- package/dist/test/tailscale-rebind.test.js +79 -0
- package/dist/test/terminal-cursor.test.js +61 -0
- package/dist/test/terminal-resync.test.js +23 -0
- package/dist/test/terminal-size.test.js +36 -0
- package/dist/test/thin-cli.test.js +382 -0
- package/dist/test/token-bootstrap.test.js +272 -0
- package/dist/test/token-devices.test.js +646 -0
- package/dist/test/token-lapse.test.js +94 -0
- package/dist/test/token-ring.test.js +188 -0
- package/dist/test/token-rotate-cli.test.js +58 -0
- package/dist/test/token-routes.test.js +95 -0
- package/dist/test/tool-uses.test.js +1006 -0
- package/dist/test/usage-ledger.test.js +126 -0
- package/dist/test/view-url.test.js +627 -0
- package/dist/test/wiki-links.test.js +57 -0
- package/dist/test/wiki-sources.test.js +85 -0
- package/dist/test/wiki-window.test.js +31 -0
- package/dist/test/wiki.test.js +399 -0
- package/dist/test/wordpiece.test.js +68 -0
- package/dist/test/wrangler-route.test.js +27 -0
- package/dist/tls.js +148 -0
- package/dist/tmux.js +213 -0
- package/dist/user-agent.js +46 -0
- package/docker-entrypoint.sh +154 -0
- package/docs/README.md +3 -0
- package/docs/absorb.md +231 -0
- package/docs/cost-ledger.md +125 -0
- package/docs/daemon-api.md +189 -0
- package/docs/diagrams.md +163 -0
- package/docs/doctor.md +36 -0
- package/docs/export-scope.md +69 -0
- package/docs/files-view.md +133 -0
- package/docs/link-previews.md +38 -0
- package/docs/math.md +32 -0
- package/docs/mcp.md +69 -0
- package/docs/memory-search.md +75 -0
- package/docs/mentions.md +101 -0
- package/docs/multi-brane.md +14 -0
- package/docs/passkeys.md +165 -0
- package/docs/presence.md +202 -0
- package/docs/releasing.md +104 -0
- package/docs/rendering-warnings.md +22 -0
- package/docs/request-layer.md +72 -0
- package/docs/selection-store.md +50 -0
- package/docs/session-affinity.md +61 -0
- package/docs/terminal-mirroring.md +243 -0
- package/docs/token-rotation.md +431 -0
- package/docs/view-urls.md +134 -0
- package/docs/web-app.md +23 -0
- package/docs/wiki.md +43 -0
- package/expo/dist/_expo/static/js/web/entry-e94247f119fdcd3b424a1764233a41d2.js +1099 -0
- package/expo/dist/apple-touch-icon.png +0 -0
- package/expo/dist/favicon.svg +3 -0
- package/expo/dist/index.html +177 -0
- package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/expo/dist/katex/katex.min.css +1 -0
- package/expo/dist/manifest.json +16 -0
- package/expo/dist/metadata.json +1 -0
- package/expo/dist/og/membrane.v1.png +0 -0
- package/expo/dist/pdf.min.mjs +28 -0
- package/expo/dist/pdf.worker.min.mjs +28 -0
- package/expo/dist/sw.js +154 -0
- package/package.json +79 -27
- package/protocols/LIVE_loop.md +407 -0
- package/protocols/bebrief.md +41 -0
- package/protocols/digest.md +231 -0
- package/protocols/fly.md +266 -0
- package/protocols/idle.md +81 -0
- package/protocols/keeper.md +829 -0
- package/protocols/markdown.md +80 -0
- package/protocols/memory.md +419 -0
- package/protocols/mentions.md +93 -0
- package/protocols/payloads.md +53 -0
- package/protocols/prelude.md +91 -0
- package/protocols/ramify.md +105 -0
- package/protocols/review.md +356 -0
- package/protocols/rrr.md +232 -0
- package/protocols/scribe.md +429 -0
- package/protocols/summarize.md +203 -0
- package/protocols/twin.md +174 -0
- package/protocols/wiki.md +438 -0
- package/protocols/worker.md +38 -0
- package/wiki/shared/append-only-event-log.md +148 -0
- package/wiki/shared/index.md +53 -0
- package/wiki/shared/owning-your-narratives.md +161 -0
- package/wiki/shared/premature-ontologies.md +435 -0
- package/wiki/shared/sedimentary-superintelligence.md +128 -0
- package/wiki/shared/sequestering-the-narrative.md +143 -0
- package/wiki/shared/why-wikis-work.md +216 -0
- package/.travis.yml +0 -7
- package/lib/brane.cjs.js +0 -12
- package/lib/brane.cjs.js.map +0 -1
- package/lib/brane.d.ts +0 -11
- package/lib/brane.js +0 -6
- package/lib/brane.js.map +0 -1
- package/lib/tests.d.ts +0 -1
- package/lib/tests.js +0 -8
- package/lib/tests.js.map +0 -1
- package/rollup.config.js +0 -21
- package/scripts/test.sh +0 -8
- package/src/brane.ts +0 -21
- package/src/tests.ts +0 -8
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Protocol-command linkification: turns `/ramify` in prose into a link that
|
|
2
|
+
* opens the protocol document, the same way `markdown-nids.ts` turns
|
|
3
|
+
* `claude-4c7289d1` into a link to that narrative.
|
|
4
|
+
*
|
|
5
|
+
* Only commands naming a protocol that actually exists are linked, so the
|
|
6
|
+
* slashes that pepper ordinary agent chatter — paths, dates, `and/or` — stay
|
|
7
|
+
* prose. That known-set check does most of the work; the boundary rule below
|
|
8
|
+
* handles the rest.
|
|
9
|
+
*/
|
|
10
|
+
import { refLinks } from "./markdown-refs.js";
|
|
11
|
+
/** A slash command: `/` then a name of the shape protocol files carry
|
|
12
|
+
* (`ramify`, `LIVE_loop`, `reviewer.draft`). */
|
|
13
|
+
export const PROTOCOL_PATTERN = "/([A-Za-z][A-Za-z0-9_.-]*)";
|
|
14
|
+
export function braneProtocolUrl(name) {
|
|
15
|
+
return `brane://protocol/${name}`;
|
|
16
|
+
}
|
|
17
|
+
/** Trailing characters a sentence lends to a command it ends: `read /ramify.`
|
|
18
|
+
* matches `/ramify.`, which is not a protocol. Shave them one at a time
|
|
19
|
+
* rather than up front, because `reviewer.draft` needs its dot. */
|
|
20
|
+
const TRAILING = ".,:;!?-";
|
|
21
|
+
/** Rejects a candidate by what sits next to it. A command stands alone: it
|
|
22
|
+
* follows the start of a run or a space/bracket, never a path segment
|
|
23
|
+
* (`src/memory`, `~/memory`, `and/or`), and nothing path-like follows it
|
|
24
|
+
* (`/memory/index.ts`). Lookbehind would express the first half, but Safari
|
|
25
|
+
* only learned it in 16.4 and this ships to phones. */
|
|
26
|
+
function boundary(before, after) {
|
|
27
|
+
if (before && /[A-Za-z0-9_./\\~-]/.test(before))
|
|
28
|
+
return false;
|
|
29
|
+
if (after === "/")
|
|
30
|
+
return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/** Rewrites `/protocol` commands into links, as an mdast-util-from-markdown
|
|
34
|
+
* extension. `hrefFor` defaults to the `brane://protocol/<name>` URL the app's
|
|
35
|
+
* markdown renderer matches on. */
|
|
36
|
+
export function protocolLinks(knownProtocols, hrefFor = braneProtocolUrl) {
|
|
37
|
+
if (knownProtocols.size === 0)
|
|
38
|
+
return { transforms: [] };
|
|
39
|
+
return refLinks({
|
|
40
|
+
pattern: PROTOCOL_PATTERN,
|
|
41
|
+
boundary,
|
|
42
|
+
resolve: (match) => {
|
|
43
|
+
let name = match[1];
|
|
44
|
+
while (name && !knownProtocols.has(name) && TRAILING.includes(name[name.length - 1])) {
|
|
45
|
+
name = name.slice(0, -1);
|
|
46
|
+
}
|
|
47
|
+
if (!name || !knownProtocols.has(name))
|
|
48
|
+
return null;
|
|
49
|
+
return { text: `/${name}`, href: hrefFor(name) };
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=markdown-protocols.js.map
|
|
@@ -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
|