@theoremjs/react 0.3.0 → 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/README.md +728 -202
- package/client/composer-hints.js +1 -1
- package/client/decision-images.d.ts +76 -0
- package/client/decision-images.js +139 -0
- package/client/decision-transport.d.ts +26 -4
- package/client/decision-transport.js +79 -6
- package/client/embedding-input.d.ts +84 -0
- package/client/embedding-input.js +194 -0
- package/client/embedding-space.d.ts +82 -0
- package/client/embedding-space.js +389 -0
- package/client/embedding-transport.d.ts +40 -0
- package/client/embedding-transport.js +71 -0
- package/client/failure.d.ts +7 -10
- package/client/failure.js +9 -9
- package/client/host-call.d.ts +1 -1
- package/client/host-call.js +7 -2
- package/client/host-transport.d.ts +2 -0
- package/client/host-transport.js +3 -2
- package/client/index.d.ts +19 -11
- package/client/index.js +14 -10
- package/client/ink-waveform.d.ts +1 -1
- package/client/ink-waveform.js +1 -2
- package/client/live/apply-live-turn-tool-event.js +1 -1
- package/client/live/live-captions.d.ts +30 -0
- package/client/live/live-captions.js +105 -1
- package/client/live/live-mic-forward.d.ts +1 -0
- package/client/live/live-mic-forward.js +11 -2
- package/client/live/live-page-tool.d.ts +34 -0
- package/client/live/live-page-tool.js +11 -0
- package/client/live/live-state.d.ts +4 -2
- package/client/live/live-state.js +2 -0
- package/client/live/live-video.js +1 -1
- package/client/live/run-live-tool-call.d.ts +7 -2
- package/client/live/run-live-tool-call.js +48 -10
- package/client/live/use-live-caption-log.d.ts +17 -0
- package/client/live/use-live-caption-log.js +51 -0
- package/client/live-client.d.ts +52 -4
- package/client/live-client.js +250 -59
- package/client/live-messages.d.ts +51 -13
- package/client/live-messages.js +42 -9
- package/client/page-dispatch.d.ts +15 -0
- package/client/page-dispatch.js +63 -0
- package/client/run-commit.d.ts +16 -43
- package/client/run-commit.js +26 -101
- package/client/run-session.d.ts +40 -41
- package/client/run-session.js +104 -112
- package/client/schema-fields.js +27 -6
- package/client/shaped-data.js +37 -8
- package/client/source-citations.js +1 -1
- package/client/space-geometry.d.ts +39 -0
- package/client/space-geometry.js +183 -0
- package/client/state-fields.js +1 -1
- package/client/tool-result.js +30 -12
- package/client/tool-resume.d.ts +18 -1
- package/client/tool-resume.js +4 -1
- package/client/trace-feed.d.ts +1 -1
- package/client/trace-story.d.ts +2 -2
- package/client/trace-story.js +43 -19
- package/client/trace-view.d.ts +2 -2
- package/client/trace-view.js +56 -16
- package/client/transcript-groups.d.ts +28 -2
- package/client/transcript-groups.js +63 -13
- package/client/transcript-schema.d.ts +10 -0
- package/client/transcript-schema.js +120 -0
- package/client/transport.d.ts +114 -26
- package/client/transport.js +106 -32
- package/client/turn-client.d.ts +17 -8
- package/client/turn-client.js +26 -31
- package/client/voice-recorder.d.ts +1 -1
- package/client/voice-recorder.js +2 -5
- package/client/wire-line.js +5 -5
- package/client/worklets/mic-capture.js +9 -9
- package/components/InkWaveform.d.ts +12 -1
- package/components/InkWaveform.js +65 -42
- package/components/live/use-live-runner-controls.d.ts +3 -4
- package/components/live/use-live-runner-controls.js +6 -10
- package/components/live/use-live-runner-model.d.ts +10 -6
- package/components/live/use-live-runner-model.js +29 -14
- package/components/live/use-live-runner-ui.d.ts +10 -8
- package/components/live/use-live-runner-ui.js +19 -11
- package/components/live/use-live-session-client.d.ts +4 -4
- package/components/live/use-live-session-client.js +11 -8
- package/components/use-composer-voice.d.ts +4 -4
- package/components/use-composer-voice.js +8 -8
- package/components/use-editor-selection.js +4 -1
- package/components/use-voice-playback.d.ts +2 -1
- package/components/use-voice-playback.js +4 -4
- package/hooks/use-described.d.ts +5 -1
- package/hooks/use-described.js +11 -9
- package/hooks/use-embedding-index.d.ts +27 -0
- package/hooks/use-embedding-index.js +43 -0
- package/hooks/use-one-call.d.ts +31 -0
- package/hooks/use-one-call.js +76 -0
- package/hooks/use-page-tool-warnings.d.ts +6 -0
- package/hooks/use-page-tool-warnings.js +25 -0
- package/hooks/use-second-ticker.d.ts +2 -0
- package/hooks/use-second-ticker.js +13 -0
- package/hooks/use-theorem-chat-actions.d.ts +25 -21
- package/hooks/use-theorem-chat-actions.js +67 -45
- package/hooks/use-theorem-chat-state.d.ts +6 -6
- package/hooks/use-theorem-chat-state.js +13 -9
- package/hooks/use-theorem-chat.d.ts +48 -14
- package/hooks/use-theorem-chat.js +329 -141
- package/hooks/use-theorem-decision.d.ts +5 -10
- package/hooks/use-theorem-decision.js +4 -67
- package/hooks/use-theorem-embedding.d.ts +18 -0
- package/hooks/use-theorem-embedding.js +12 -0
- package/hooks/use-theorem-host.d.ts +5 -4
- package/hooks/use-theorem-host.js +26 -8
- package/hooks/use-theorem-interface.d.ts +6 -3
- package/hooks/use-theorem-interface.js +12 -22
- package/index.d.ts +6 -3
- package/index.js +5 -2
- package/live.d.ts +1 -0
- package/live.js +1 -1
- package/package.json +6 -5
- package/server/checkpoint-ownership.d.ts +6 -0
- package/server/checkpoint-ownership.js +42 -0
- package/server/conversation-store.d.ts +92 -0
- package/server/conversation-store.js +131 -0
- package/server/conversation.d.ts +193 -0
- package/server/conversation.js +552 -0
- package/server/decision-handler.d.ts +12 -4
- package/server/decision-handler.js +29 -38
- package/server/embedding-handler.d.ts +33 -0
- package/server/embedding-handler.js +45 -0
- package/server/handler.d.ts +58 -20
- package/server/handler.js +441 -238
- package/server/mod.d.ts +15 -6
- package/server/mod.js +13 -5
- package/server/page-dispatch.d.ts +22 -0
- package/server/page-dispatch.js +127 -0
- package/server/request-body.d.ts +2 -0
- package/server/request-body.js +42 -0
- package/server/request-check.d.ts +6 -1
- package/server/request-check.js +27 -12
- package/server/session-authority.d.ts +8 -0
- package/server/session-authority.js +78 -0
- package/server/session-boundary.d.ts +26 -0
- package/server/session-boundary.js +64 -0
- package/server/session-identity.d.ts +16 -0
- package/server/session-identity.js +41 -0
- package/server/session-limits.d.ts +12 -0
- package/server/session-limits.js +59 -0
- package/server/session-store.d.ts +69 -20
- package/server/session-store.js +220 -5
- package/server/single-action.d.ts +21 -0
- package/server/single-action.js +30 -0
- package/server/steer-inbox.js +1 -1
- package/server/tool-operations.d.ts +6 -0
- package/server/tool-operations.js +132 -0
- package/server/turn-input.d.ts +10 -0
- package/server/turn-input.js +29 -0
- package/styles/ink-controls.css +78 -78
- package/ui/CardWall.d.ts +10 -0
- package/ui/CardWall.js +61 -0
- package/ui/ChatComposerBar.d.ts +28 -5
- package/ui/ChatComposerBar.js +85 -30
- package/ui/ChatTranscript.d.ts +14 -1
- package/ui/ChatTranscript.js +110 -57
- package/ui/ConsoleFrame.d.ts +61 -4
- package/ui/ConsoleFrame.js +109 -9
- package/ui/DecisionAnswers.d.ts +40 -2
- package/ui/DecisionAnswers.js +166 -35
- package/ui/DecisionImages.d.ts +16 -0
- package/ui/DecisionImages.js +46 -0
- package/ui/EmbeddingDocuments.d.ts +78 -0
- package/ui/EmbeddingDocuments.js +278 -0
- package/ui/EmbeddingQuery.d.ts +27 -0
- package/ui/EmbeddingQuery.js +95 -0
- package/ui/EmbeddingResults.d.ts +41 -0
- package/ui/EmbeddingResults.js +165 -0
- package/ui/EmbeddingSpace.d.ts +26 -0
- package/ui/EmbeddingSpace.js +106 -0
- package/ui/LiveCaptionsPanel.d.ts +22 -0
- package/ui/LiveCaptionsPanel.js +27 -0
- package/ui/LiveRunner.d.ts +6 -4
- package/ui/LiveRunner.js +42 -55
- package/ui/PaneState.d.ts +33 -0
- package/ui/PaneState.js +50 -0
- package/ui/SchemaFields.d.ts +1 -1
- package/ui/SchemaFields.js +54 -26
- package/ui/ShapedData.js +73 -28
- package/ui/SidePanel.d.ts +74 -5
- package/ui/SidePanel.js +82 -9
- package/ui/StateFields.d.ts +10 -2
- package/ui/StateFields.js +56 -13
- package/ui/TheoremChat.d.ts +65 -14
- package/ui/TheoremChat.js +66 -78
- package/ui/TheoremDecision.d.ts +12 -20
- package/ui/TheoremDecision.js +78 -35
- package/ui/TheoremEmbedding.d.ts +27 -0
- package/ui/TheoremEmbedding.js +122 -0
- package/ui/TheoremHost.d.ts +22 -5
- package/ui/TheoremHost.js +127 -37
- package/ui/ToolGateCard.d.ts +5 -1
- package/ui/ToolGateCard.js +30 -23
- package/ui/ToolResult.js +12 -10
- package/ui/ToolResultCharts.js +29 -12
- package/ui/TraceCharts.d.ts +2 -2
- package/ui/TraceCharts.js +48 -17
- package/ui/TraceGuardrails.d.ts +3 -2
- package/ui/TraceGuardrails.js +24 -6
- package/ui/TraceInspector.d.ts +11 -1
- package/ui/TraceInspector.js +45 -28
- package/ui/TraceInspectorPanel.d.ts +31 -9
- package/ui/TraceInspectorPanel.js +77 -39
- package/ui/TraceOverview.d.ts +7 -3
- package/ui/TraceOverview.js +44 -14
- package/ui/TraceSpanDetail.js +24 -22
- package/ui/TraceStory.js +9 -8
- package/ui/TraceValues.js +19 -9
- package/ui/TraceWaterfall.js +12 -9
- package/ui/VoiceNote.js +4 -4
- package/ui/approval-heading.d.ts +4 -0
- package/ui/approval-heading.js +9 -0
- package/ui/arrive.d.ts +18 -0
- package/ui/arrive.js +46 -0
- package/ui/built/theme.css +86 -68
- package/ui/built/theme.css.d.ts +9 -0
- package/ui/built/theorem.d.ts +3 -3
- package/ui/built/theorem.js +34 -5
- package/ui/built/theorem.variants.d.ts +8 -2
- package/ui/code-language.d.ts +12 -0
- package/ui/code-language.js +50 -0
- package/ui/disclosure-motion.js +53 -9
- package/ui/embedding-space-gl.d.ts +37 -0
- package/ui/embedding-space-gl.js +663 -0
- package/ui/embedding-space-view.d.ts +8 -0
- package/ui/embedding-space-view.js +8 -0
- package/ui/glide.d.ts +7 -0
- package/ui/glide.js +44 -0
- package/ui/icons.js +2 -1
- package/ui/index.d.ts +20 -9
- package/ui/index.js +20 -10
- package/ui/labels-provider.js +5 -2
- package/ui/labels.d.ts +374 -85
- package/ui/labels.js +1028 -188
- package/ui/markdown-plugins.d.ts +9 -0
- package/ui/markdown-plugins.js +13 -0
- package/ui/mcp-icon.d.ts +6 -0
- package/ui/mcp-icon.js +8 -0
- package/ui/row-keys.d.ts +19 -0
- package/ui/row-keys.js +33 -0
- package/ui/theme.d.ts +1 -1
- package/ui/theme.js +2 -2
- package/ui/theorem-theme.js +32 -10
- package/ui/trace-palette.d.ts +2 -16
- package/ui/trace-palette.js +2 -16
- package/ui/trace-patterns.d.ts +11 -3
- package/ui/trace-patterns.js +16 -3
- package/ui/transcript-copy-text.js +1 -0
- package/ui/wall-columns.d.ts +8 -0
- package/ui/wall-columns.js +19 -0
- package/client/walk-away.d.ts +0 -34
- package/client/walk-away.js +0 -48
- package/server/walk-away.d.ts +0 -22
- package/server/walk-away.js +0 -41
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draws the embedding space with WebGL2 and nothing else: anti-aliased lines,
|
|
3
|
+
* solid dots and the query's white dot, laid over the card with premultiplied
|
|
4
|
+
* alpha so the card shows through. Dots cover whatever lies behind them, and
|
|
5
|
+
* every line stops just short of the dot it reaches; the far side of the
|
|
6
|
+
* ball draws smaller and fainter. No React here: the component owns one
|
|
7
|
+
* renderer and feeds it.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { DOT_FLOATS, FRAME_SLOT, queryGeometry, SEGMENT_FLOATS, spaceGeometry, } from "../client/space-geometry.js";
|
|
12
|
+
/** Slot states per texture row. */
|
|
13
|
+
const STATE_ROW = 1024;
|
|
14
|
+
/** Field of view, in radians. */
|
|
15
|
+
/** Wide enough that the ball, whose farthest document is on its rim, fits with a margin at the camera's distance. */
|
|
16
|
+
const FOV = (44 * Math.PI) / 180;
|
|
17
|
+
/** The camera's distance from the ball's centre: close, so depth reads as perspective. */
|
|
18
|
+
const DISTANCE = 3.2;
|
|
19
|
+
/** Seconds a cluster's lines take to draw, and the most a cluster waits to start. */
|
|
20
|
+
const GROW_S = 0.9;
|
|
21
|
+
const GROW_STAGGER_S = 0.6;
|
|
22
|
+
/** Seconds a query's lines take to reach its matches. */
|
|
23
|
+
const REVEAL_S = 0.7;
|
|
24
|
+
/** Seconds the sphere takes to turn a query to face the viewer. */
|
|
25
|
+
const TURN_S = 1.2;
|
|
26
|
+
/** Radians per second the sphere turns on its own. */
|
|
27
|
+
const SPIN = 0.05;
|
|
28
|
+
/** How far, in CSS pixels, the pointer reaches for a document. */
|
|
29
|
+
const PICK_PX = 18;
|
|
30
|
+
/** Squared CSS pixels within which two dots under the pointer are told apart by depth. */
|
|
31
|
+
const PICK_TIE = 16;
|
|
32
|
+
/** How bright a document reads when a query leaves it out. */
|
|
33
|
+
const DIMMED = 0.2;
|
|
34
|
+
/** How fast a flung sphere slows, per second, and the speed in radians per second below which it stops. */
|
|
35
|
+
const COAST_DECAY = 4;
|
|
36
|
+
const COAST_STOP = 0.01;
|
|
37
|
+
/** Milliseconds a drag may pause before release and still fling the sphere. */
|
|
38
|
+
const FLING_MS = 80;
|
|
39
|
+
const PREAMBLE = '#version 300 es\nprecision highp float;\nprecision highp int;\n';
|
|
40
|
+
const STATE_GLSL = `
|
|
41
|
+
uniform sampler2D states;
|
|
42
|
+
vec4 stateOf(float i) {
|
|
43
|
+
if (i < -0.5) return vec4(1.0, 1.0, 0.0, 0.0);
|
|
44
|
+
int k = int(i + 0.5);
|
|
45
|
+
return texelFetch(states, ivec2(k % ${STATE_ROW}, k / ${STATE_ROW}), 0);
|
|
46
|
+
}
|
|
47
|
+
// How near the viewer a point sits, from 0 at the ball's back to 1 at its front.
|
|
48
|
+
float facing(vec3 p) {
|
|
49
|
+
return clamp(0.5 + 0.5 * p.z, 0.0, 1.0);
|
|
50
|
+
}
|
|
51
|
+
float facingFade(vec3 p) {
|
|
52
|
+
return mix(0.18, 1.0, facing(p));
|
|
53
|
+
}
|
|
54
|
+
float facingSize(vec3 p) {
|
|
55
|
+
return mix(0.6, 1.1, facing(p));
|
|
56
|
+
}
|
|
57
|
+
// The axes' circles fade with the square of depth, so their front reads strongly and their back recedes.
|
|
58
|
+
float frameFade(vec3 p) {
|
|
59
|
+
float k = mix(0.2, 1.0, facing(p));
|
|
60
|
+
return mix(0.24, 1.0, k * k);
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
const SEGMENT_VERTEX = `${PREAMBLE}
|
|
64
|
+
layout(location = 0) in vec2 corner;
|
|
65
|
+
layout(location = 1) in vec3 from;
|
|
66
|
+
layout(location = 2) in vec3 to;
|
|
67
|
+
layout(location = 3) in vec3 colour;
|
|
68
|
+
layout(location = 4) in float alpha;
|
|
69
|
+
layout(location = 5) in float width;
|
|
70
|
+
layout(location = 6) in vec2 slots;
|
|
71
|
+
layout(location = 7) in float along;
|
|
72
|
+
layout(location = 8) in vec2 gaps;
|
|
73
|
+
uniform mat4 projection;
|
|
74
|
+
uniform mat3 turn;
|
|
75
|
+
uniform vec2 viewport;
|
|
76
|
+
uniform float ratio;
|
|
77
|
+
uniform float reveal;
|
|
78
|
+
${STATE_GLSL}
|
|
79
|
+
out vec3 vColour;
|
|
80
|
+
out float vAlpha;
|
|
81
|
+
out float vAcross;
|
|
82
|
+
out float vHalf;
|
|
83
|
+
out float vAlong;
|
|
84
|
+
out vec3 vEnds;
|
|
85
|
+
void main() {
|
|
86
|
+
vec4 sa = stateOf(slots.x);
|
|
87
|
+
vec4 sb = stateOf(slots.y);
|
|
88
|
+
float grow = min(sa.g, sb.g);
|
|
89
|
+
float hover = max(sa.b, sb.b);
|
|
90
|
+
float shown = step(along, grow + 1e-4) * step(along, reveal + 1e-4);
|
|
91
|
+
vec3 pa = turn * from;
|
|
92
|
+
vec3 pb = turn * to;
|
|
93
|
+
vec4 ca = projection * vec4(pa, 1.0);
|
|
94
|
+
vec4 cb = projection * vec4(pb, 1.0);
|
|
95
|
+
vec2 d = (cb.xy / cb.w - ca.xy / ca.w) * viewport;
|
|
96
|
+
float len = length(d);
|
|
97
|
+
vec2 dir = len > 1e-4 ? d / len : vec2(1.0, 0.0);
|
|
98
|
+
vec2 normal = vec2(-dir.y, dir.x);
|
|
99
|
+
vec3 p = corner.x < 0.5 ? pa : pb;
|
|
100
|
+
vec4 c = corner.x < 0.5 ? ca : cb;
|
|
101
|
+
// invariant: only the axes' circles read the frame slot, and they keep one width at every depth.
|
|
102
|
+
bool frame = slots.x < ${FRAME_SLOT + 0.5};
|
|
103
|
+
// why: a line thinner than a device pixel is drawn one pixel wide and fainter, so it stays smooth.
|
|
104
|
+
float full = width * ratio * (frame ? 1.0 : facingSize(p)) * (1.0 + 0.8 * hover);
|
|
105
|
+
float half_ = 0.5 * max(full, 1.0);
|
|
106
|
+
float reach = half_ + 1.0;
|
|
107
|
+
c.xy += normal * corner.y * reach * 2.0 / viewport * c.w;
|
|
108
|
+
gl_Position = c;
|
|
109
|
+
vAcross = corner.y * reach;
|
|
110
|
+
vHalf = half_;
|
|
111
|
+
vAlong = corner.x * len;
|
|
112
|
+
// invariant: each end's gap grows with its dot, as the dot grows when hovered.
|
|
113
|
+
vEnds = vec3(
|
|
114
|
+
len,
|
|
115
|
+
gaps.x * ratio * facingSize(pa) * (1.0 + 0.6 * sa.b),
|
|
116
|
+
gaps.y * ratio * facingSize(pb) * (1.0 + 0.6 * sb.b)
|
|
117
|
+
);
|
|
118
|
+
vColour = colour;
|
|
119
|
+
vAlpha = min(alpha * shown * min(sa.r, sb.r) * (1.0 + hover), 1.0) * (frame ? frameFade(p) : facingFade(p)) * min(full, 1.0);
|
|
120
|
+
}`;
|
|
121
|
+
const SEGMENT_FRAGMENT = `${PREAMBLE}
|
|
122
|
+
in vec3 vColour;
|
|
123
|
+
in float vAlpha;
|
|
124
|
+
in float vAcross;
|
|
125
|
+
in float vHalf;
|
|
126
|
+
in float vAlong;
|
|
127
|
+
in vec3 vEnds;
|
|
128
|
+
out vec4 outColour;
|
|
129
|
+
void main() {
|
|
130
|
+
float clear = clamp(vAlong - vEnds.y + 0.5, 0.0, 1.0) * clamp(vEnds.x - vAlong - vEnds.z + 0.5, 0.0, 1.0);
|
|
131
|
+
float a = vAlpha * clear * clamp(vHalf + 0.5 - abs(vAcross), 0.0, 1.0);
|
|
132
|
+
if (a < 0.002) discard;
|
|
133
|
+
outColour = vec4(vColour * a, a);
|
|
134
|
+
}`;
|
|
135
|
+
const DOT_VERTEX = `${PREAMBLE}
|
|
136
|
+
layout(location = 0) in vec2 corner;
|
|
137
|
+
layout(location = 1) in vec3 position;
|
|
138
|
+
layout(location = 2) in vec3 colour;
|
|
139
|
+
layout(location = 3) in float alpha;
|
|
140
|
+
layout(location = 4) in float radius;
|
|
141
|
+
layout(location = 5) in float slot;
|
|
142
|
+
layout(location = 6) in float along;
|
|
143
|
+
uniform mat4 projection;
|
|
144
|
+
uniform mat3 turn;
|
|
145
|
+
uniform vec2 viewport;
|
|
146
|
+
uniform float ratio;
|
|
147
|
+
uniform float reveal;
|
|
148
|
+
${STATE_GLSL}
|
|
149
|
+
out vec3 vColour;
|
|
150
|
+
out float vAlpha;
|
|
151
|
+
out vec2 vAt;
|
|
152
|
+
out float vRadius;
|
|
153
|
+
void main() {
|
|
154
|
+
vec4 s = stateOf(slot);
|
|
155
|
+
float shown = step(along, s.g + 1e-4) * step(along, reveal + 1e-4);
|
|
156
|
+
vec3 p = turn * position;
|
|
157
|
+
vec4 c = projection * vec4(p, 1.0);
|
|
158
|
+
float size = ratio * facingSize(p);
|
|
159
|
+
float r = radius * size * (1.0 + 0.6 * s.b);
|
|
160
|
+
float reach = r + 1.0;
|
|
161
|
+
c.xy += corner * reach * 2.0 / viewport * c.w;
|
|
162
|
+
gl_Position = c;
|
|
163
|
+
vAt = corner * reach;
|
|
164
|
+
vRadius = r;
|
|
165
|
+
vColour = colour;
|
|
166
|
+
vAlpha = alpha * shown * min(s.r, 1.0) * facingFade(p);
|
|
167
|
+
}`;
|
|
168
|
+
const DOT_FRAGMENT = `${PREAMBLE}
|
|
169
|
+
in vec3 vColour;
|
|
170
|
+
in float vAlpha;
|
|
171
|
+
in vec2 vAt;
|
|
172
|
+
in float vRadius;
|
|
173
|
+
out vec4 outColour;
|
|
174
|
+
void main() {
|
|
175
|
+
float d = length(vAt);
|
|
176
|
+
float a = vAlpha * clamp(vRadius + 0.5 - d, 0.0, 1.0);
|
|
177
|
+
// invariant: only what a dot covers writes depth, so the lines behind it and nothing else are hidden.
|
|
178
|
+
if (a < 0.002) discard;
|
|
179
|
+
outColour = vec4(vColour * a, a);
|
|
180
|
+
}`;
|
|
181
|
+
/** Why the space could not be drawn here. */
|
|
182
|
+
class SpaceUnavailable extends Error {
|
|
183
|
+
}
|
|
184
|
+
function compile(gl, vertex, fragment) {
|
|
185
|
+
const program = gl.createProgram();
|
|
186
|
+
for (const [type, source] of [
|
|
187
|
+
[gl.VERTEX_SHADER, vertex],
|
|
188
|
+
[gl.FRAGMENT_SHADER, fragment],
|
|
189
|
+
]) {
|
|
190
|
+
const shader = gl.createShader(type);
|
|
191
|
+
if (!shader)
|
|
192
|
+
throw new SpaceUnavailable('shader');
|
|
193
|
+
gl.shaderSource(shader, source);
|
|
194
|
+
gl.compileShader(shader);
|
|
195
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
|
|
196
|
+
throw new SpaceUnavailable(gl.getShaderInfoLog(shader) ?? 'shader');
|
|
197
|
+
gl.attachShader(program, shader);
|
|
198
|
+
gl.deleteShader(shader);
|
|
199
|
+
}
|
|
200
|
+
gl.linkProgram(program);
|
|
201
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
|
|
202
|
+
throw new SpaceUnavailable(gl.getProgramInfoLog(program) ?? 'program');
|
|
203
|
+
return program;
|
|
204
|
+
}
|
|
205
|
+
function quatMultiply(a, b) {
|
|
206
|
+
return [
|
|
207
|
+
a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
|
|
208
|
+
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
|
|
209
|
+
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
|
|
210
|
+
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
function quatAxis(axis, angle) {
|
|
214
|
+
const s = Math.sin(angle / 2);
|
|
215
|
+
return [axis[0] * s, axis[1] * s, axis[2] * s, Math.cos(angle / 2)];
|
|
216
|
+
}
|
|
217
|
+
function quatNormalise(q) {
|
|
218
|
+
const length = Math.hypot(...q) || 1;
|
|
219
|
+
return [q[0] / length, q[1] / length, q[2] / length, q[3] / length];
|
|
220
|
+
}
|
|
221
|
+
function quatSlerp(a, b, t) {
|
|
222
|
+
let cos = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
|
|
223
|
+
const to = cos < 0 ? [-b[0], -b[1], -b[2], -b[3]] : [...b];
|
|
224
|
+
cos = Math.abs(cos);
|
|
225
|
+
if (cos > 0.9995)
|
|
226
|
+
return quatNormalise(a.map((v, i) => v + (to[i] - v) * t));
|
|
227
|
+
const angle = Math.acos(cos);
|
|
228
|
+
const sin = Math.sin(angle);
|
|
229
|
+
const wa = Math.sin((1 - t) * angle) / sin;
|
|
230
|
+
const wb = Math.sin(t * angle) / sin;
|
|
231
|
+
return a.map((v, i) => v * wa + to[i] * wb);
|
|
232
|
+
}
|
|
233
|
+
/** The turn that carries one unit direction onto another. */
|
|
234
|
+
function quatBetween(from, to) {
|
|
235
|
+
const cos = from[0] * to[0] + from[1] * to[1] + from[2] * to[2];
|
|
236
|
+
if (cos < -0.9999)
|
|
237
|
+
return quatAxis(Math.abs(from[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0], Math.PI);
|
|
238
|
+
const axis = [
|
|
239
|
+
from[1] * to[2] - from[2] * to[1],
|
|
240
|
+
from[2] * to[0] - from[0] * to[2],
|
|
241
|
+
from[0] * to[1] - from[1] * to[0],
|
|
242
|
+
];
|
|
243
|
+
return quatNormalise([axis[0], axis[1], axis[2], 1 + cos]);
|
|
244
|
+
}
|
|
245
|
+
/** A unit quaternion as the column-major 3×3 matrix GLSL takes. */
|
|
246
|
+
function quatMatrix([x, y, z, w]) {
|
|
247
|
+
return new Float32Array([
|
|
248
|
+
1 - 2 * (y * y + z * z),
|
|
249
|
+
2 * (x * y + z * w),
|
|
250
|
+
2 * (x * z - y * w),
|
|
251
|
+
2 * (x * y - z * w),
|
|
252
|
+
1 - 2 * (x * x + z * z),
|
|
253
|
+
2 * (y * z + x * w),
|
|
254
|
+
2 * (x * z + y * w),
|
|
255
|
+
2 * (y * z - x * w),
|
|
256
|
+
1 - 2 * (x * x + y * y),
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
function turnPoint(m, p) {
|
|
260
|
+
return [
|
|
261
|
+
m[0] * p[0] + m[3] * p[1] + m[6] * p[2],
|
|
262
|
+
m[1] * p[0] + m[4] * p[1] + m[7] * p[2],
|
|
263
|
+
m[2] * p[0] + m[5] * p[1] + m[8] * p[2],
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
/** Perspective looking down -z from the camera, with the sphere's centre at the origin in front of it. */
|
|
267
|
+
function projectionMatrix(aspect) {
|
|
268
|
+
const f = 1 / Math.tan(FOV / 2);
|
|
269
|
+
const near = 0.5;
|
|
270
|
+
const far = 20;
|
|
271
|
+
// invariant: the camera sits DISTANCE along +z, folded into the matrix as a translation.
|
|
272
|
+
const m = new Float32Array(16);
|
|
273
|
+
m[0] = aspect >= 1 ? f / aspect : f;
|
|
274
|
+
m[5] = aspect >= 1 ? f : f * aspect;
|
|
275
|
+
m[10] = (far + near) / (near - far);
|
|
276
|
+
m[11] = -1;
|
|
277
|
+
m[14] = (2 * far * near) / (near - far) + m[10] * -DISTANCE;
|
|
278
|
+
m[15] = DISTANCE;
|
|
279
|
+
return m;
|
|
280
|
+
}
|
|
281
|
+
function easeInOut(t) {
|
|
282
|
+
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* One renderer for one canvas. Throws {@link SpaceUnavailable} when the
|
|
286
|
+
* browser cannot draw it, so the caller shows the list instead.
|
|
287
|
+
*/
|
|
288
|
+
export function createSpaceRenderer(canvas, { still }) {
|
|
289
|
+
const context = canvas.getContext('webgl2', {
|
|
290
|
+
alpha: true,
|
|
291
|
+
premultipliedAlpha: true,
|
|
292
|
+
antialias: true,
|
|
293
|
+
depth: true,
|
|
294
|
+
stencil: false,
|
|
295
|
+
});
|
|
296
|
+
if (!context)
|
|
297
|
+
throw new SpaceUnavailable('webgl2');
|
|
298
|
+
const gl = context;
|
|
299
|
+
const segmentProgram = compile(gl, SEGMENT_VERTEX, SEGMENT_FRAGMENT);
|
|
300
|
+
const dotProgram = compile(gl, DOT_VERTEX, DOT_FRAGMENT);
|
|
301
|
+
// why: WebGL names this call useProgram; one wrapper keeps the React hook rule from reading it as a hook.
|
|
302
|
+
const activate = gl.useProgram.bind(gl);
|
|
303
|
+
const uniforms = new Map();
|
|
304
|
+
const uniform = (program, name) => {
|
|
305
|
+
let names = uniforms.get(program);
|
|
306
|
+
if (!names) {
|
|
307
|
+
names = new Map();
|
|
308
|
+
uniforms.set(program, names);
|
|
309
|
+
}
|
|
310
|
+
if (!names.has(name))
|
|
311
|
+
names.set(name, gl.getUniformLocation(program, name));
|
|
312
|
+
return names.get(name) ?? null;
|
|
313
|
+
};
|
|
314
|
+
const strip = gl.createBuffer();
|
|
315
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, strip);
|
|
316
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, -1, 1, -1, 0, 1, 1, 1]), gl.STATIC_DRAW);
|
|
317
|
+
const square = gl.createBuffer();
|
|
318
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, square);
|
|
319
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
|
|
320
|
+
/** A vertex array for instances of one kind: the shared corners, then each instance's floats by width. */
|
|
321
|
+
function layer(corners, floats, widths) {
|
|
322
|
+
const vao = gl.createVertexArray();
|
|
323
|
+
const buffer = gl.createBuffer();
|
|
324
|
+
gl.bindVertexArray(vao);
|
|
325
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, corners);
|
|
326
|
+
gl.enableVertexAttribArray(0);
|
|
327
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
328
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
329
|
+
let offset = 0;
|
|
330
|
+
widths.forEach((width, i) => {
|
|
331
|
+
gl.enableVertexAttribArray(i + 1);
|
|
332
|
+
gl.vertexAttribPointer(i + 1, width, gl.FLOAT, false, floats * 4, offset * 4);
|
|
333
|
+
gl.vertexAttribDivisor(i + 1, 1);
|
|
334
|
+
offset += width;
|
|
335
|
+
});
|
|
336
|
+
gl.bindVertexArray(null);
|
|
337
|
+
return { vao, buffer, count: 0 };
|
|
338
|
+
}
|
|
339
|
+
const SEGMENT_WIDTHS = [3, 3, 3, 1, 1, 2, 1, 2];
|
|
340
|
+
const DOT_WIDTHS = [3, 3, 1, 1, 1, 1];
|
|
341
|
+
const layers = {
|
|
342
|
+
segments: layer(strip, SEGMENT_FLOATS, SEGMENT_WIDTHS),
|
|
343
|
+
dots: layer(square, DOT_FLOATS, DOT_WIDTHS),
|
|
344
|
+
querySegments: layer(strip, SEGMENT_FLOATS, SEGMENT_WIDTHS),
|
|
345
|
+
queryDots: layer(square, DOT_FLOATS, DOT_WIDTHS),
|
|
346
|
+
};
|
|
347
|
+
function fill(target, data, floats) {
|
|
348
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, target.buffer);
|
|
349
|
+
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
|
|
350
|
+
target.count = data.length / floats;
|
|
351
|
+
}
|
|
352
|
+
function fillGeometry(geometry, segments, dots) {
|
|
353
|
+
fill(segments, geometry.segments, SEGMENT_FLOATS);
|
|
354
|
+
fill(dots, geometry.dots, DOT_FLOATS);
|
|
355
|
+
}
|
|
356
|
+
const stateTexture = gl.createTexture();
|
|
357
|
+
let stateData = new Float32Array(4);
|
|
358
|
+
function stateSize(count) {
|
|
359
|
+
const width = Math.max(1, Math.min(count, STATE_ROW));
|
|
360
|
+
const height = Math.max(1, Math.ceil(count / STATE_ROW));
|
|
361
|
+
gl.bindTexture(gl.TEXTURE_2D, stateTexture);
|
|
362
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, width, height, 0, gl.RGBA, gl.FLOAT, null);
|
|
363
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
364
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
365
|
+
stateData = new Float32Array(width * height * 4);
|
|
366
|
+
return { width, height };
|
|
367
|
+
}
|
|
368
|
+
let stateShape = stateSize(1);
|
|
369
|
+
let space = null;
|
|
370
|
+
let bornAt = [];
|
|
371
|
+
let bright = new Float32Array(0);
|
|
372
|
+
let hover = new Float32Array(0);
|
|
373
|
+
let brightTarget = new Float32Array(0);
|
|
374
|
+
let focus = { query: null, matches: [], hovered: null };
|
|
375
|
+
let queryAt = -Infinity;
|
|
376
|
+
let turn = quatNormalise(quatMultiply(quatAxis([1, 0, 0], 0.35), quatAxis([0, 1, 0], -0.5)));
|
|
377
|
+
/** Where the sphere turns from and to while it turns to face the query. */
|
|
378
|
+
let turning = null;
|
|
379
|
+
let turnAt = 0;
|
|
380
|
+
let dragging = false;
|
|
381
|
+
/** How fast the sphere turns about its y and x axes, in radians per second, from the last drag; it coasts on this after release. */
|
|
382
|
+
let coast = [0, 0];
|
|
383
|
+
let draggedAt = 0;
|
|
384
|
+
let last = performance.now();
|
|
385
|
+
let frame = 0;
|
|
386
|
+
let visible = true;
|
|
387
|
+
let destroyed = false;
|
|
388
|
+
function aimAt(point) {
|
|
389
|
+
const turned = turnPoint(quatMatrix(turn), point);
|
|
390
|
+
const length = Math.hypot(...turned);
|
|
391
|
+
// why: a query at the centre faces the viewer from every turn.
|
|
392
|
+
if (length < 1e-6)
|
|
393
|
+
return;
|
|
394
|
+
turning = {
|
|
395
|
+
from: turn,
|
|
396
|
+
to: quatNormalise(quatMultiply(quatBetween(turned.map((v) => v / length), [0, 0, 1]), turn)),
|
|
397
|
+
};
|
|
398
|
+
turnAt = performance.now();
|
|
399
|
+
}
|
|
400
|
+
/** Every document's brightness for the focus: a query's matches full, the rest dimmed. */
|
|
401
|
+
function retarget() {
|
|
402
|
+
if (!space)
|
|
403
|
+
return;
|
|
404
|
+
const closeness = new Map(focus.matches.map((m) => [m.index, m.closeness]));
|
|
405
|
+
for (const document of space.documents) {
|
|
406
|
+
const close = closeness.get(document.index);
|
|
407
|
+
brightTarget[document.index] = !focus.query || close !== undefined ? 1 : DIMMED;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function resize() {
|
|
411
|
+
const ratio = Math.min(globalThis.devicePixelRatio || 1, 2);
|
|
412
|
+
const width = Math.max(1, Math.round(canvas.clientWidth * ratio));
|
|
413
|
+
const height = Math.max(1, Math.round(canvas.clientHeight * ratio));
|
|
414
|
+
if (canvas.width === width && canvas.height === height)
|
|
415
|
+
return;
|
|
416
|
+
canvas.width = width;
|
|
417
|
+
canvas.height = height;
|
|
418
|
+
}
|
|
419
|
+
function turnBy(aboutY, aboutX) {
|
|
420
|
+
turn = quatNormalise(quatMultiply(quatMultiply(quatAxis([0, 1, 0], aboutY), quatAxis([1, 0, 0], aboutX)), turn));
|
|
421
|
+
}
|
|
422
|
+
function coasting() {
|
|
423
|
+
return Math.hypot(...coast) > COAST_STOP;
|
|
424
|
+
}
|
|
425
|
+
function easeSlots(ease) {
|
|
426
|
+
const want = focus.hovered;
|
|
427
|
+
for (let i = 0; i < bright.length; i += 1) {
|
|
428
|
+
bright[i] += (brightTarget[i] - bright[i]) * ease;
|
|
429
|
+
hover[i] += ((want === i ? 1 : 0) - hover[i]) * ease;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function faceQuery(now, { from, to }) {
|
|
433
|
+
const t = still ? 1 : Math.min((now - turnAt) / (TURN_S * 1000), 1);
|
|
434
|
+
turn = quatSlerp(from, to, easeInOut(t));
|
|
435
|
+
if (t >= 1)
|
|
436
|
+
turning = null;
|
|
437
|
+
}
|
|
438
|
+
function turnSphere(now, dt) {
|
|
439
|
+
if (turning)
|
|
440
|
+
faceQuery(now, turning);
|
|
441
|
+
else if (dragging)
|
|
442
|
+
return;
|
|
443
|
+
else if (coasting()) {
|
|
444
|
+
turnBy(coast[0] * dt, coast[1] * dt);
|
|
445
|
+
const decay = Math.exp(-COAST_DECAY * dt);
|
|
446
|
+
coast = [coast[0] * decay, coast[1] * decay];
|
|
447
|
+
}
|
|
448
|
+
else if (!still && !focus.query)
|
|
449
|
+
turnBy(SPIN * dt, 0);
|
|
450
|
+
}
|
|
451
|
+
function step(now) {
|
|
452
|
+
const dt = Math.min((now - last) / 1000, 0.1);
|
|
453
|
+
last = now;
|
|
454
|
+
// why: with reduced motion, brightness changes at once rather than fading.
|
|
455
|
+
easeSlots(still ? 1 : 1 - Math.exp(-dt * 7));
|
|
456
|
+
turnSphere(now, dt);
|
|
457
|
+
}
|
|
458
|
+
/** Whether the sphere turns, or a cluster or the query is still growing in. */
|
|
459
|
+
function animating(now) {
|
|
460
|
+
if (dragging || turning || coasting())
|
|
461
|
+
return true;
|
|
462
|
+
if (still)
|
|
463
|
+
return false;
|
|
464
|
+
if (!focus.query)
|
|
465
|
+
return true;
|
|
466
|
+
return now - queryAt < REVEAL_S * 1000 || bornAt.some((at) => now < at + GROW_S * 1000);
|
|
467
|
+
}
|
|
468
|
+
/** Whether every slot's brightness and hover have reached where they are going. */
|
|
469
|
+
function eased() {
|
|
470
|
+
for (let i = 0; i < bright.length; i += 1) {
|
|
471
|
+
const hovering = focus.hovered === i ? 1 : 0;
|
|
472
|
+
if (Math.abs(brightTarget[i] - bright[i]) > 0.005 || Math.abs(hovering - hover[i]) > 0.005)
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
/** Whether anything is still moving; a settled sphere draws again only on change. */
|
|
478
|
+
function settled(now) {
|
|
479
|
+
return !animating(now) && eased();
|
|
480
|
+
}
|
|
481
|
+
function writeStates(now) {
|
|
482
|
+
for (let i = 0; i < bright.length; i += 1) {
|
|
483
|
+
const grown = still ? 1 : Math.min(Math.max((now - bornAt[i]) / (GROW_S * 1000), 0), 1);
|
|
484
|
+
stateData[i * 4] = bright[i];
|
|
485
|
+
stateData[i * 4 + 1] = 1 - (1 - grown) ** 3;
|
|
486
|
+
stateData[i * 4 + 2] = hover[i];
|
|
487
|
+
stateData[i * 4 + 3] = 0;
|
|
488
|
+
}
|
|
489
|
+
gl.bindTexture(gl.TEXTURE_2D, stateTexture);
|
|
490
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, stateShape.width, stateShape.height, gl.RGBA, gl.FLOAT, stateData);
|
|
491
|
+
}
|
|
492
|
+
function draw(program, part, reveal, view) {
|
|
493
|
+
if (part.count === 0)
|
|
494
|
+
return;
|
|
495
|
+
activate(program);
|
|
496
|
+
gl.uniformMatrix4fv(uniform(program, 'projection'), false, view.projection);
|
|
497
|
+
gl.uniformMatrix3fv(uniform(program, 'turn'), false, view.turn);
|
|
498
|
+
gl.uniform2f(uniform(program, 'viewport'), canvas.width, canvas.height);
|
|
499
|
+
gl.uniform1f(uniform(program, 'ratio'), view.ratio);
|
|
500
|
+
gl.uniform1f(uniform(program, 'reveal'), reveal);
|
|
501
|
+
gl.uniform1i(uniform(program, 'states'), 0);
|
|
502
|
+
gl.bindVertexArray(part.vao);
|
|
503
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, part.count);
|
|
504
|
+
}
|
|
505
|
+
function render(now) {
|
|
506
|
+
resize();
|
|
507
|
+
step(now);
|
|
508
|
+
writeStates(now);
|
|
509
|
+
const view = {
|
|
510
|
+
projection: projectionMatrix(canvas.width / canvas.height),
|
|
511
|
+
turn: quatMatrix(turn),
|
|
512
|
+
ratio: canvas.width / Math.max(canvas.clientWidth, 1),
|
|
513
|
+
};
|
|
514
|
+
const shown = still ? 1 : Math.min((now - queryAt) / (REVEAL_S * 1000), 1);
|
|
515
|
+
const reveal = shown >= 1 ? 2 : shown;
|
|
516
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
517
|
+
gl.clearColor(0, 0, 0, 0);
|
|
518
|
+
gl.clearDepth(1);
|
|
519
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
520
|
+
gl.enable(gl.BLEND);
|
|
521
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
522
|
+
gl.enable(gl.DEPTH_TEST);
|
|
523
|
+
gl.depthFunc(gl.LEQUAL);
|
|
524
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
525
|
+
gl.bindTexture(gl.TEXTURE_2D, stateTexture);
|
|
526
|
+
// invariant: dots first, writing depth; then lines, tested against it, so a dot covers the lines behind it.
|
|
527
|
+
gl.depthMask(true);
|
|
528
|
+
draw(dotProgram, layers.dots, 2, view);
|
|
529
|
+
draw(dotProgram, layers.queryDots, reveal, view);
|
|
530
|
+
gl.depthMask(false);
|
|
531
|
+
draw(segmentProgram, layers.segments, 2, view);
|
|
532
|
+
draw(segmentProgram, layers.querySegments, reveal, view);
|
|
533
|
+
gl.depthMask(true);
|
|
534
|
+
gl.disable(gl.DEPTH_TEST);
|
|
535
|
+
gl.disable(gl.BLEND);
|
|
536
|
+
}
|
|
537
|
+
function loop(now) {
|
|
538
|
+
frame = 0;
|
|
539
|
+
if (destroyed || !visible)
|
|
540
|
+
return;
|
|
541
|
+
render(now);
|
|
542
|
+
if (!settled(now))
|
|
543
|
+
frame = requestAnimationFrame(loop);
|
|
544
|
+
}
|
|
545
|
+
function wake() {
|
|
546
|
+
if (destroyed || !visible || frame !== 0)
|
|
547
|
+
return;
|
|
548
|
+
last = performance.now();
|
|
549
|
+
frame = requestAnimationFrame(loop);
|
|
550
|
+
}
|
|
551
|
+
const watcher = new IntersectionObserver(([entry]) => {
|
|
552
|
+
visible = (entry?.isIntersecting ?? true) && document.visibilityState === 'visible';
|
|
553
|
+
if (visible)
|
|
554
|
+
wake();
|
|
555
|
+
});
|
|
556
|
+
watcher.observe(canvas);
|
|
557
|
+
const onVisibility = () => {
|
|
558
|
+
visible = document.visibilityState === 'visible';
|
|
559
|
+
if (visible)
|
|
560
|
+
wake();
|
|
561
|
+
};
|
|
562
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
563
|
+
const sizer = new ResizeObserver(() => wake());
|
|
564
|
+
sizer.observe(canvas);
|
|
565
|
+
function applyQuery() {
|
|
566
|
+
if (!space || !focus.query) {
|
|
567
|
+
layers.querySegments.count = 0;
|
|
568
|
+
layers.queryDots.count = 0;
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
fillGeometry(queryGeometry(space, focus.query, focus.matches), layers.querySegments, layers.queryDots);
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
setSpace(next) {
|
|
575
|
+
space = next;
|
|
576
|
+
const now = performance.now();
|
|
577
|
+
const count = next.documents.length;
|
|
578
|
+
stateShape = stateSize(count);
|
|
579
|
+
// invariant: a cluster's documents grow together; clusters start in turn.
|
|
580
|
+
const startOf = (k) => now + (GROW_STAGGER_S * 1000 * k) / Math.max(next.clusters.length, 1);
|
|
581
|
+
bornAt = next.documents.map((d) => startOf(d.cluster));
|
|
582
|
+
bright = new Float32Array(count).fill(1);
|
|
583
|
+
hover = new Float32Array(count);
|
|
584
|
+
brightTarget = new Float32Array(count).fill(1);
|
|
585
|
+
fillGeometry(spaceGeometry(next), layers.segments, layers.dots);
|
|
586
|
+
retarget();
|
|
587
|
+
applyQuery();
|
|
588
|
+
wake();
|
|
589
|
+
},
|
|
590
|
+
setFocus(next) {
|
|
591
|
+
const moved = next.query !== focus.query &&
|
|
592
|
+
(!next.query || !focus.query || next.query.some((v, i) => v !== focus.query?.[i]));
|
|
593
|
+
focus = next;
|
|
594
|
+
retarget();
|
|
595
|
+
if (moved) {
|
|
596
|
+
applyQuery();
|
|
597
|
+
queryAt = performance.now();
|
|
598
|
+
if (next.query)
|
|
599
|
+
aimAt(next.query);
|
|
600
|
+
}
|
|
601
|
+
wake();
|
|
602
|
+
},
|
|
603
|
+
pick(x, y) {
|
|
604
|
+
if (!space)
|
|
605
|
+
return null;
|
|
606
|
+
const ratio = canvas.width / Math.max(canvas.clientWidth, 1);
|
|
607
|
+
const projection = projectionMatrix(canvas.width / canvas.height);
|
|
608
|
+
const turnMatrix = quatMatrix(turn);
|
|
609
|
+
let best = null;
|
|
610
|
+
let bestDistance = PICK_PX * PICK_PX;
|
|
611
|
+
let bestDepth = -Infinity;
|
|
612
|
+
for (const document of space.documents) {
|
|
613
|
+
const p = turnPoint(turnMatrix, document.position);
|
|
614
|
+
const w = projection[11] * p[2] + projection[15];
|
|
615
|
+
const sx = ((projection[0] * p[0]) / w + 1) * 0.5 * canvas.width;
|
|
616
|
+
const sy = (1 - (projection[5] * p[1]) / w) * 0.5 * canvas.height;
|
|
617
|
+
const d = (sx / ratio - x) ** 2 + (sy / ratio - y) ** 2;
|
|
618
|
+
// why: of two dots under the pointer, within a dot's width, the nearer one is the one seen.
|
|
619
|
+
const nearer = d < bestDistance + PICK_TIE && p[2] > bestDepth;
|
|
620
|
+
if (d < bestDistance - PICK_TIE || (d < PICK_PX * PICK_PX && nearer)) {
|
|
621
|
+
bestDistance = d;
|
|
622
|
+
bestDepth = p[2];
|
|
623
|
+
best = document.index;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return best;
|
|
627
|
+
},
|
|
628
|
+
drag(dx, dy) {
|
|
629
|
+
const now = performance.now();
|
|
630
|
+
const dt = Math.max((now - draggedAt) / 1000, 1 / 120);
|
|
631
|
+
dragging = true;
|
|
632
|
+
turning = null;
|
|
633
|
+
draggedAt = now;
|
|
634
|
+
const speed = 3 / Math.max(Math.min(canvas.clientWidth, canvas.clientHeight), 1);
|
|
635
|
+
turnBy(dx * speed, dy * speed);
|
|
636
|
+
// why: half this move and half the run before it, so one jittery event does not decide the fling.
|
|
637
|
+
coast = [0.5 * coast[0] + (0.5 * dx * speed) / dt, 0.5 * coast[1] + (0.5 * dy * speed) / dt];
|
|
638
|
+
wake();
|
|
639
|
+
},
|
|
640
|
+
release() {
|
|
641
|
+
dragging = false;
|
|
642
|
+
if (still || performance.now() - draggedAt > FLING_MS)
|
|
643
|
+
coast = [0, 0];
|
|
644
|
+
wake();
|
|
645
|
+
},
|
|
646
|
+
destroy() {
|
|
647
|
+
destroyed = true;
|
|
648
|
+
cancelAnimationFrame(frame);
|
|
649
|
+
watcher.disconnect();
|
|
650
|
+
sizer.disconnect();
|
|
651
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
652
|
+
for (const l of Object.values(layers)) {
|
|
653
|
+
gl.deleteVertexArray(l.vao);
|
|
654
|
+
gl.deleteBuffer(l.buffer);
|
|
655
|
+
}
|
|
656
|
+
for (const program of [segmentProgram, dotProgram])
|
|
657
|
+
gl.deleteProgram(program);
|
|
658
|
+
gl.deleteBuffer(strip);
|
|
659
|
+
gl.deleteBuffer(square);
|
|
660
|
+
gl.deleteTexture(stateTexture);
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The embedding space, for results to load when they first show it. The package's UI exports
|
|
3
|
+
* `EmbeddingSpace.tsx` as well, and a bundler leaves a module in its importer's chunk when it is
|
|
4
|
+
* imported both ways, so results import it through this module instead.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
export { EmbeddingSpaceView } from './EmbeddingSpace.tsx';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The embedding space, for results to load when they first show it. The package's UI exports
|
|
3
|
+
* `EmbeddingSpace.tsx` as well, and a bundler leaves a module in its importer's chunk when it is
|
|
4
|
+
* imported both ways, so results import it through this module instead.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
export { EmbeddingSpaceView } from "./EmbeddingSpace.js";
|
package/ui/glide.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Eases an element from where it sat while `held` to where it lands once `held`
|
|
4
|
+
* turns false (FLIP). Astryx has no layout-transition helper, so this uses its
|
|
5
|
+
* `--duration-medium` / `--ease-standard` tokens. `onLand` runs as it lands.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useGlide<T extends HTMLElement>(held: boolean, onLand?: () => void): RefObject<T | null>;
|