@inerrata-corporation/errata 2.0.21 → 2.0.22-dev.4020
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/consolidate-worker.mjs +443 -187
- package/errata.mjs +5929 -4474
- package/generalize-worker.mjs +17430 -0
- package/install.ps1 +1 -1
- package/package.json +2 -1
- package/pass-worker.mjs +3101 -2653
- package/resources/claude-mods/errata-minimap/.claude-plugin/plugin.json +2 -2
- package/resources/claude-mods/errata-minimap/hooks/bookcase.ts +450 -0
- package/resources/claude-mods/errata-minimap/hooks/chains.ts +427 -0
- package/resources/claude-mods/errata-minimap/hooks/graph.ts +349 -0
- package/resources/claude-mods/errata-minimap/hooks/minimap.ts +39 -351
- package/resources/claude-mods/errata-minimap/hooks/register.tsx +440 -148
- package/resources/claude-mods/errata-minimap/hooks/tags.ts +129 -0
- package/resources/claude-mods/errata-minimap/types/index.d.ts +42 -8
- package/resources/context-plugin/.codex-plugin/plugin.json +1 -1
- package/resources/context-plugin/SHA256SUMS +1 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import type { ChainFacts, MapGraph, MapNode, Scope } from '../types'
|
|
2
|
+
import type { JudgmentKind } from './minimap'
|
|
3
|
+
import { displayName, shortName } from './minimap'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The graph view (`/errata-view graph`): the original minimap — the burst as a
|
|
7
|
+
* radial graph, the seed at the centre, one ring per hop, braille edges
|
|
8
|
+
* coloured by how far each prior travelled. The library (bookcase.ts) is the
|
|
9
|
+
* default; this stays for people who read structure better than shelves.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type RadialNode = MapNode & { x: number; y: number }
|
|
13
|
+
|
|
14
|
+
export type RadialGraph = {
|
|
15
|
+
nodes: RadialNode[]
|
|
16
|
+
/** Index pairs into `nodes`; `true` = an edge of the walk, `false` = a cross edge. */
|
|
17
|
+
edges: [number, number, boolean][]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Radial layout of one catalogued burst: each node in its parent's angular
|
|
22
|
+
* sector, one ring per hop, and within a ring a higher score sits closer in.
|
|
23
|
+
* Positions are in the unit square. `layoutBurst` already lists every node
|
|
24
|
+
* right after its parent, so one pass places them all.
|
|
25
|
+
*/
|
|
26
|
+
export function layoutRadial(map: MapGraph, cross: ReadonlyArray<readonly [string, string]> = []): RadialGraph {
|
|
27
|
+
const maxHops = Math.max(1, ...map.nodes.map(n => n.hops))
|
|
28
|
+
const scores = map.nodes.filter(n => !n.isSeed).map(n => n.score)
|
|
29
|
+
const [lo, hi] = [Math.min(...scores), Math.max(...scores)]
|
|
30
|
+
const norm = (s: number) => (hi > lo ? (s - lo) / (hi - lo) : 1)
|
|
31
|
+
|
|
32
|
+
const kids = new Map<number, number[]>()
|
|
33
|
+
map.nodes.forEach((n, i) => {
|
|
34
|
+
if (n.isSeed) return
|
|
35
|
+
const p = n.parent >= 0 && n.parent < map.nodes.length ? n.parent : 0
|
|
36
|
+
kids.set(p, [...(kids.get(p) ?? []), i])
|
|
37
|
+
})
|
|
38
|
+
const sector = new Map<number, [number, number]>([[0, [0, Math.PI * 2]]])
|
|
39
|
+
const nodes: RadialNode[] = map.nodes.map(n => ({ ...n, x: 0.5, y: 0.5 }))
|
|
40
|
+
const edges: [number, number, boolean][] = []
|
|
41
|
+
map.nodes.forEach((_, p) => {
|
|
42
|
+
const children = kids.get(p) ?? []
|
|
43
|
+
const [a0, a1] = sector.get(p) ?? [0, Math.PI * 2]
|
|
44
|
+
const span = (a1 - a0) / Math.max(1, children.length)
|
|
45
|
+
children.forEach((i, k) => {
|
|
46
|
+
const n = map.nodes[i]!
|
|
47
|
+
const angle = a0 + span * (k + 0.5) - Math.PI / 2
|
|
48
|
+
const r = 0.48 * (n.hops / maxHops) * (0.72 + 0.28 * (1 - norm(n.score)))
|
|
49
|
+
nodes[i] = { ...n, x: 0.5 + r * Math.cos(angle), y: 0.5 + r * Math.sin(angle) }
|
|
50
|
+
sector.set(i, [a0 + span * k, a0 + span * (k + 1)])
|
|
51
|
+
edges.push([p, i, true])
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const index = new Map(map.nodes.map((n, i) => [n.id, i]))
|
|
56
|
+
const seen = new Set(edges.map(([a, b]) => key(a, b)))
|
|
57
|
+
for (const [s, t] of cross) {
|
|
58
|
+
const a = index.get(s), b = index.get(t)
|
|
59
|
+
if (a === undefined || b === undefined || a === b || seen.has(key(a, b))) continue
|
|
60
|
+
seen.add(key(a, b))
|
|
61
|
+
edges.push([a, b, false])
|
|
62
|
+
}
|
|
63
|
+
return { nodes, edges }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function key(a: number, b: number): string {
|
|
67
|
+
return a < b ? `${a},${b}` : `${b},${a}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The real edges among a burst's nodes, from the `errata_neighbors` answers the
|
|
72
|
+
* library already fetched to complete its chains — the graph view's cross
|
|
73
|
+
* edges cost no call of their own.
|
|
74
|
+
*/
|
|
75
|
+
export function crossEdges(map: MapGraph, facts: Readonly<Record<string, ChainFacts | null>>): [string, string][] {
|
|
76
|
+
const ids = new Set(map.nodes.map(n => n.id))
|
|
77
|
+
const out: [string, string][] = []
|
|
78
|
+
for (const f of Object.values(facts)) {
|
|
79
|
+
if (!f || !ids.has(f.id)) continue
|
|
80
|
+
for (const far of [...f.causes, ...f.fixes, ...f.problems, ...f.patterns]) {
|
|
81
|
+
if (ids.has(far.id) && far.id !== f.id) out.push([f.id, far.id])
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const DEFAULT = 0x01000000
|
|
88
|
+
// Bright enough to read on a dark terminal: the first cut's #3a3f4b vanished there.
|
|
89
|
+
const EDGE_WALK = 0x8a96b4
|
|
90
|
+
const EDGE_CROSS = 0x5c6680
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* An edge takes the colour of the membrane its far end lives in, so the map
|
|
94
|
+
* reads "how far did this prior travel to reach me": machine-only, the
|
|
95
|
+
* project / team / org membranes, or the public Errata Network.
|
|
96
|
+
*/
|
|
97
|
+
export const SCOPE_COLOR: Record<Scope, number> = {
|
|
98
|
+
machine: 0x8a96b4, // slate — never left this machine
|
|
99
|
+
project: 0x5fa8ff, // blue
|
|
100
|
+
team: 0x3fc5b0, // teal
|
|
101
|
+
org: 0xf0b34a, // amber
|
|
102
|
+
public: 0xd27cf0, // magenta — The Errata Network
|
|
103
|
+
}
|
|
104
|
+
export const SCOPE_ORDER: Scope[] = ['machine', 'project', 'team', 'org', 'public']
|
|
105
|
+
|
|
106
|
+
/** The tier an edge is coloured by: its non-seed end, the farther-travelled when both say. */
|
|
107
|
+
export function edgeScope(a: MapNode, b: MapNode): Scope | undefined {
|
|
108
|
+
const ends = [a, b].filter(v => !v.isSeed && v.scope).map(v => v.scope!)
|
|
109
|
+
return ends.sort((p, q) => SCOPE_ORDER.indexOf(q) - SCOPE_ORDER.indexOf(p))[0]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function dim(color: number): number {
|
|
113
|
+
return (((color >> 16) & 255) * 0.6) << 16 | (((color >> 8) & 255) * 0.6) << 8 | ((color & 255) * 0.6)
|
|
114
|
+
}
|
|
115
|
+
const NAME = 0x9aa3b5
|
|
116
|
+
|
|
117
|
+
export const LABEL_COLOR: Record<string, number> = {
|
|
118
|
+
Problem: 0xe06c75,
|
|
119
|
+
Vulnerability: 0xff5f87,
|
|
120
|
+
Solution: 0x98c379,
|
|
121
|
+
RootCause: 0xd19a66,
|
|
122
|
+
Pattern: 0xc678dd,
|
|
123
|
+
AntiPattern: 0xbe5046,
|
|
124
|
+
Technique: 0x56b6c2,
|
|
125
|
+
Constraint: 0xe5c07b,
|
|
126
|
+
File: 0x61afef,
|
|
127
|
+
Function: 0x7fb8d8,
|
|
128
|
+
Method: 0x7fb8d8,
|
|
129
|
+
Module: 0xa9b1c4,
|
|
130
|
+
Package: 0xa9b1c4,
|
|
131
|
+
Const: 0x8fbfa0,
|
|
132
|
+
Variable: 0x8fbfa0,
|
|
133
|
+
Interface: 0xb4a7d6,
|
|
134
|
+
Class: 0xb4a7d6,
|
|
135
|
+
Type: 0xb4a7d6,
|
|
136
|
+
Comment: 0x7c8494,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const LABEL_GLYPH: Record<string, number> = {
|
|
140
|
+
Problem: 0x25c6, // ◆
|
|
141
|
+
Vulnerability: 0x25c6,
|
|
142
|
+
Solution: 0x25cf, // ●
|
|
143
|
+
RootCause: 0x25b2, // ▲
|
|
144
|
+
Pattern: 0x2726, // ✦
|
|
145
|
+
AntiPattern: 0x2726,
|
|
146
|
+
Technique: 0x25c7, // ◇
|
|
147
|
+
Constraint: 0x25a0, // ■
|
|
148
|
+
File: 0x25aa, // ▪
|
|
149
|
+
// Code structure a burst walks through (CALLS / IMPORTS / DEFINES / CONTAINS).
|
|
150
|
+
Function: 0x0192, // ƒ
|
|
151
|
+
Method: 0x0192,
|
|
152
|
+
Module: 0x229e, // ⊞
|
|
153
|
+
Package: 0x229e,
|
|
154
|
+
Const: 0x2261, // ≡
|
|
155
|
+
Variable: 0x2261,
|
|
156
|
+
Interface: 0x25a1, // □
|
|
157
|
+
Class: 0x25a1,
|
|
158
|
+
Type: 0x25a1,
|
|
159
|
+
Comment: 0x00b6, // ¶
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function glyphOf(label: string): number {
|
|
163
|
+
return LABEL_GLYPH[label] ?? 0x2022 // •
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function colorOf(label: string): number {
|
|
167
|
+
return LABEL_COLOR[label] ?? 0x6b7280
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Verdict colours on the graph's own (One Dark) palette. */
|
|
171
|
+
export const VERDICT_COLOR: Record<JudgmentKind, number> = { cite: 0x56b6c2, confirm: 0x98c379, fix: 0x98c379, refute: 0xe06c75 }
|
|
172
|
+
|
|
173
|
+
/** What "transparent" fades toward: the background of the terminal theme. */
|
|
174
|
+
export const DARK_BG = 0x1a1b1e
|
|
175
|
+
export const LIGHT_BG = 0xf7f7f5
|
|
176
|
+
|
|
177
|
+
/** Fraction of a colour kept at the weakest signal: faint, never gone. */
|
|
178
|
+
const FLOOR = 0.28
|
|
179
|
+
|
|
180
|
+
/** Signal strength per node, 0..1 over this burst's score range; the seed is full strength. */
|
|
181
|
+
export function strengths(graph: RadialGraph): number[] {
|
|
182
|
+
const scores = graph.nodes.filter(v => !v.isSeed).map(v => v.score)
|
|
183
|
+
const [lo, hi] = [Math.min(...scores), Math.max(...scores)]
|
|
184
|
+
return graph.nodes.map(v => (v.isSeed ? 1 : hi > lo ? (v.score - lo) / (hi - lo) : 1))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Blends `color` toward `bg` as signal weakens: strength 1 keeps it, 0 keeps FLOOR of it. */
|
|
188
|
+
export function fade(color: number, strength: number, bg: number): number {
|
|
189
|
+
const keep = FLOOR + (1 - FLOOR) * Math.max(0, Math.min(1, strength))
|
|
190
|
+
const ch = (c: number, shift: number) => Math.round(((c >> shift) & 255) * keep + ((bg >> shift) & 255) * (1 - keep))
|
|
191
|
+
return (ch(color, 16) << 16) | (ch(color, 8) << 8) | ch(color, 0)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** How far the reveal has run: routes grow out ring by ring (`grow`), then names type out (`type`). Both 0..1. */
|
|
195
|
+
export type Frame = { grow: number; type: number }
|
|
196
|
+
|
|
197
|
+
export type RasterOptions = {
|
|
198
|
+
frame?: Frame
|
|
199
|
+
/** The theme background weak signal fades toward. */
|
|
200
|
+
bg?: number
|
|
201
|
+
/** A node the agent is bursting off: drawn inverted, its route to the seed lit. */
|
|
202
|
+
highlight?: string
|
|
203
|
+
isPulseOn?: boolean
|
|
204
|
+
/** Priors the agent just judged: each pulses in its verdict's colour. */
|
|
205
|
+
marks?: Record<string, JudgmentKind>
|
|
206
|
+
isMarkOn?: boolean
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const HIGHLIGHT = 0xffd75f
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The graph as Raster cells (`[codePoint, fg, bg]` u32 triplets, row-major,
|
|
213
|
+
* base64) at one frame of the reveal. Positions and label slots are planned on
|
|
214
|
+
* the finished map, so an animated frame only ever draws a prefix of it —
|
|
215
|
+
* nothing jumps as it fills in.
|
|
216
|
+
*/
|
|
217
|
+
export function rasterizeGraph(graph: RadialGraph, columns: number, rows: number, options: RasterOptions = {}): string {
|
|
218
|
+
const { frame = { grow: 1, type: 1 }, bg = DARK_BG, highlight, isPulseOn = true, marks = {}, isMarkOn = true } = options
|
|
219
|
+
const markColor = (id: string): number | undefined => {
|
|
220
|
+
const kind = marks[id]
|
|
221
|
+
return kind ? VERDICT_COLOR[kind] : undefined
|
|
222
|
+
}
|
|
223
|
+
const words = new Uint32Array(columns * rows * 3)
|
|
224
|
+
for (let i = 0; i < columns * rows; i++) words.set([0x20, DEFAULT, DEFAULT], i * 3)
|
|
225
|
+
const occupied = new Uint8Array(columns * rows)
|
|
226
|
+
const cx = (x: number) => 1 + Math.round(x * Math.max(0, columns - 3))
|
|
227
|
+
const cy = (y: number) => Math.round(y * Math.max(0, rows - 1))
|
|
228
|
+
const put = (x: number, y: number, ch: number, fg: number, weight: number, cellBg = DEFAULT) => {
|
|
229
|
+
if (x < 0 || y < 0 || x >= columns || y >= rows) return false
|
|
230
|
+
const i = y * columns + x
|
|
231
|
+
if (occupied[i]! > weight) return false
|
|
232
|
+
occupied[i] = weight
|
|
233
|
+
words.set([ch, fg, cellBg], i * 3)
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const pos: [number, number][] = graph.nodes.map(v => [cx(v.x), cy(v.y)])
|
|
238
|
+
// Two nodes in one cell: walk the later one outward to the nearest free cell.
|
|
239
|
+
const taken = new Set<number>()
|
|
240
|
+
graph.nodes.forEach((_, i) => {
|
|
241
|
+
let [x, y] = pos[i]!
|
|
242
|
+
for (let r = 0; taken.has(y * columns + x) && r < 6; r++) {
|
|
243
|
+
const ring = [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [-1, -1], [1, -1], [-1, 1]].map(([dx, dy]) => [x + dx! * (r + 1), y + dy! * (r + 1)] as const)
|
|
244
|
+
const free = ring.find(([a, b]) => a >= 0 && b >= 0 && a < columns && b < rows && !taken.has(b * columns + a))
|
|
245
|
+
if (free) [x, y] = free
|
|
246
|
+
}
|
|
247
|
+
taken.add(y * columns + x)
|
|
248
|
+
pos[i] = [x, y]
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const strength = strengths(graph)
|
|
252
|
+
// The highlighted node and the walk edges from it back to the seed.
|
|
253
|
+
const lit = graph.nodes.findIndex(v => v.id === highlight)
|
|
254
|
+
const route = new Set<number>()
|
|
255
|
+
for (let at = lit, guard = 0; at > 0 && guard < 16; guard++) {
|
|
256
|
+
const k = graph.edges.findIndex(([, c, isWalk]) => isWalk && c === at)
|
|
257
|
+
if (k < 0) break
|
|
258
|
+
route.add(k)
|
|
259
|
+
at = graph.edges[k]![0]
|
|
260
|
+
}
|
|
261
|
+
const depth = Math.max(1, ...graph.nodes.map(v => v.hops))
|
|
262
|
+
// How much of the walk edge INTO node i has grown: ring h fills during the h-th slice of `grow`.
|
|
263
|
+
const reach = (i: number) => (graph.nodes[i]!.isSeed ? 1 : Math.max(0, Math.min(1, frame.grow * depth - (graph.nodes[i]!.hops - 1))))
|
|
264
|
+
|
|
265
|
+
// Label slots, planned on the finished map, best first: nodes and earlier labels block, edges do not.
|
|
266
|
+
const blocked = new Set(pos.map(([x, y]) => y * columns + x))
|
|
267
|
+
const labels: { i: number; at: number; text: string }[] = []
|
|
268
|
+
const ranked = graph.nodes.map((v, i) => [v, i] as const).filter(([v]) => !v.isSeed).sort((p, q) => q[0].score - p[0].score)
|
|
269
|
+
for (const [v, i] of ranked) {
|
|
270
|
+
const [x, y] = pos[i]!
|
|
271
|
+
const free = (from: number, len: number) => {
|
|
272
|
+
if (from < 0 || from + len > columns) return false
|
|
273
|
+
for (let k = 0; k < len; k++) if (blocked.has(y * columns + from + k)) return false
|
|
274
|
+
return true
|
|
275
|
+
}
|
|
276
|
+
const fits = (len: number) => (free(x + 2, len) ? x + 2 : free(x - 1 - len, len) ? x - 1 - len : -1)
|
|
277
|
+
let text = shortName(displayName(v.label, v.name), 18)
|
|
278
|
+
while (text.length >= 4 && fits(text.length) < 0) text = shortName(text.replace(/~$/, ''), text.length - 2)
|
|
279
|
+
const at = text.length >= 4 ? fits(text.length) : -1
|
|
280
|
+
if (at < 0) continue
|
|
281
|
+
for (let k = 0; k < text.length; k++) blocked.add(y * columns + at + k)
|
|
282
|
+
labels.push({ i, at, text })
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Edges go down at braille resolution — 2×4 dots per cell — so a line of any
|
|
286
|
+
// slope is one continuous stroke. Per-cell box glyphs could not do that: a
|
|
287
|
+
// cell is ~2× taller than wide, so a visual diagonal steps two cells sideways
|
|
288
|
+
// per row and adjacent ╱╱ never join.
|
|
289
|
+
const dots = new Uint8Array(columns * rows)
|
|
290
|
+
const tone = new Uint8Array(columns * rows) // 2 = a walk edge crosses the cell, 1 = cross edges only
|
|
291
|
+
const hue = new Uint32Array(columns * rows) // the colour of the strongest edge through the cell
|
|
292
|
+
const plot = (px: number, py: number, weight: number, color: number) => {
|
|
293
|
+
const x = px >> 1, y = py >> 2
|
|
294
|
+
if (x < 0 || y < 0 || x >= columns || y >= rows) return
|
|
295
|
+
const i = y * columns + x
|
|
296
|
+
dots[i] = dots[i]! | BRAILLE_BIT[(py & 3) * 2 + (px & 1)]!
|
|
297
|
+
if (weight >= tone[i]!) hue[i] = color
|
|
298
|
+
tone[i] = Math.max(tone[i]!, weight)
|
|
299
|
+
}
|
|
300
|
+
for (const [k, [a, b, isWalk]] of graph.edges.entries()) {
|
|
301
|
+
// A walk edge grows parent → child; a cross edge fades in while names type.
|
|
302
|
+
const progress = isWalk ? reach(b) : frame.type
|
|
303
|
+
if (progress <= 0) continue
|
|
304
|
+
const weaker = graph.nodes[a]!.isSeed ? b : graph.nodes[b]!.isSeed ? a : strength[a]! < strength[b]! ? a : b
|
|
305
|
+
const scope = edgeScope(graph.nodes[a]!, graph.nodes[b]!)
|
|
306
|
+
const base = scope ? SCOPE_COLOR[scope] : EDGE_WALK
|
|
307
|
+
const color = route.has(k) ? HIGHLIGHT : fade(isWalk ? base : scope ? dim(base) : EDGE_CROSS, strength[weaker]!, bg)
|
|
308
|
+
let x0 = pos[a]![0] * 2 + 1, y0 = pos[a]![1] * 4 + 2
|
|
309
|
+
const x1 = pos[b]![0] * 2 + 1, y1 = pos[b]![1] * 4 + 2
|
|
310
|
+
const dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0)
|
|
311
|
+
const sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1
|
|
312
|
+
const limit = Math.round(progress * Math.max(dx, -dy))
|
|
313
|
+
let err = dx + dy
|
|
314
|
+
// Cross edges are dashed so a walk edge reads as the path taken.
|
|
315
|
+
for (let step = 0; step <= limit; step++) {
|
|
316
|
+
if (isWalk || step % 4 < 2) plot(x0, y0, route.has(k) ? 4 : isWalk ? 2 : 1, color)
|
|
317
|
+
if (x0 === x1 && y0 === y1) break
|
|
318
|
+
const e2 = 2 * err
|
|
319
|
+
if (e2 >= dy) { err += dy; x0 += sx }
|
|
320
|
+
if (e2 <= dx) { err += dx; y0 += sy }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (let i = 0; i < columns * rows; i++) {
|
|
324
|
+
if (dots[i]) put(i % columns, Math.floor(i / columns), 0x2800 + dots[i]!, hue[i]!, tone[i]!)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
graph.nodes.forEach((v, i) => {
|
|
328
|
+
if (reach(i) < 1) return
|
|
329
|
+
const [x, y] = pos[i]!
|
|
330
|
+
if (v.isSeed) put(x, y, 0x25c9, 0xffd75f, 9) // ◉
|
|
331
|
+
else if (i === lit) put(x, y, glyphOf(v.label), isPulseOn ? bg : HIGHLIGHT, 10, isPulseOn ? HIGHLIGHT : DEFAULT)
|
|
332
|
+
else if (markColor(v.id) !== undefined) {
|
|
333
|
+
const c = markColor(v.id)!
|
|
334
|
+
put(x, y, glyphOf(v.label), isMarkOn ? bg : c, 10, isMarkOn ? c : DEFAULT)
|
|
335
|
+
}
|
|
336
|
+
else put(x, y, glyphOf(v.label), fade(colorOf(v.label), strength[i]!, bg), 8)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
for (const { i, at, text } of labels) {
|
|
340
|
+
if (reach(i) < 1) continue
|
|
341
|
+
const shown = Math.floor(text.length * frame.type)
|
|
342
|
+
const color = i === lit ? HIGHLIGHT : markColor(graph.nodes[i]!.id) ?? fade(NAME, strength[i]!, bg)
|
|
343
|
+
for (let k = 0; k < shown; k++) put(at + k, pos[i]![1], text.codePointAt(k)!, color, 3)
|
|
344
|
+
}
|
|
345
|
+
return new Uint8Array(words.buffer).toBase64()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Unicode braille dot bits, indexed by `row * 2 + column` within the 2×4 cell (U+2800 + bits). */
|
|
349
|
+
const BRAILLE_BIT = [0x01, 0x08, 0x02, 0x10, 0x04, 0x20, 0x40, 0x80]
|